diff --git a/playwright.config.ts b/playwright.config.ts index c779d9a..bff9f55 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -73,7 +73,6 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { command: 'pnpm build && pnpm preview --port 4173 --host 127.0.0.1', - // command: 'echo "Temporarily disabled"', url: 'http://127.0.0.1:4173/', reuseExistingServer: !process.env.CI, timeout: 1000 * 60 * 5 diff --git a/src/lib/colors/ColorPicker.svelte b/src/lib/colors/ColorPicker.svelte index d25a1c7..a689981 100644 --- a/src/lib/colors/ColorPicker.svelte +++ b/src/lib/colors/ColorPicker.svelte @@ -2,12 +2,12 @@ import I18n from '$lib/I18n.svelte'; import Modal from '$lib/Modal.svelte'; import SubmitButton from '$lib/SubmitButton.svelte'; - import type { IntRange } from '../../types/utils'; + import type { RangeRGB } from '../../types/subjectColors'; import { rgbToHex } from './hexToRgb'; - export let r: IntRange<0, 255> = 0; - export let g: IntRange<0, 255> = 0; - export let b: IntRange<0, 255> = 0; + export let r: RangeRGB = 0; + export let g: RangeRGB = 0; + export let b: RangeRGB = 0; export let open = false; diff --git a/src/lib/colors/hexToRgb.ts b/src/lib/colors/hexToRgb.ts index 68f935d..69eb959 100644 --- a/src/lib/colors/hexToRgb.ts +++ b/src/lib/colors/hexToRgb.ts @@ -1,21 +1,22 @@ -import type { IntRange } from '../../types/utils'; +import type { RGB, RangeRGB } from '../../types/subjectColors'; +import { rgbSchema } from '../../zod/subjectColor'; -type Range = IntRange<0, 255>; +export const hexToRgb = (hex: string): RGB | null => { + if (!/^#[0-9a-f]{6}$/i.test(hex)) { + return null; + } + return { + r: parseInt(hex.slice(1, 3), 16) as RangeRGB, + g: parseInt(hex.slice(3, 5), 16) as RangeRGB, + b: parseInt(hex.slice(5, 7), 16) as RangeRGB + }; +}; -export const hexToRgb = ( - hex: string -): { - r: Range; - g: Range; - b: Range; -} => ({ - r: parseInt(hex.slice(1, 3), 16) as Range, - g: parseInt(hex.slice(3, 5), 16) as Range, - b: parseInt(hex.slice(5, 7), 16) as Range -}); - -// create a function to make any decimal number a hex number with 2 digits const makeHex = (n: number): string => n.toString(16).padStart(2, '0'); -export const rgbToHex = (r: Range, g: Range, b: Range): string => - `#${makeHex(r)}${makeHex(g)}${makeHex(b)}`; +export const rgbToHex = (r: RangeRGB, g: RangeRGB, b: RangeRGB): string | null => { + const result = rgbSchema.safeParse({ r, g, b }); + if (!result.success) return null; + + return `#${makeHex(r)}${makeHex(g)}${makeHex(b)}`; +}; diff --git a/src/types/subjectColors.ts b/src/types/subjectColors.ts index 4eabcb9..194d97c 100644 --- a/src/types/subjectColors.ts +++ b/src/types/subjectColors.ts @@ -1,6 +1,6 @@ import type { IntRange } from './utils'; -type RangeRGB = IntRange<0, 255>; +export type RangeRGB = IntRange<0, 256>; export type RGB = { r: RangeRGB; diff --git a/src/zod/subjectColor.ts b/src/zod/subjectColor.ts index 8be71f6..5ad3dbc 100644 --- a/src/zod/subjectColor.ts +++ b/src/zod/subjectColor.ts @@ -1,10 +1,12 @@ import { z } from 'zod'; +export const rgbSchema = z.object({ + r: z.number().max(255).min(0), + g: z.number().max(255).min(0), + b: z.number().max(255).min(0) +}); + export const subjectColorSchema = z.object({ subject: z.string(), - color: z.object({ - r: z.number().max(255).min(0), - g: z.number().max(255).min(0), - b: z.number().max(255).min(0) - }) + color: rgbSchema }); diff --git a/tests/colors.test.ts b/tests/colors.test.ts new file mode 100644 index 0000000..d3452dc --- /dev/null +++ b/tests/colors.test.ts @@ -0,0 +1,100 @@ +// import { hexToRgb } from '$lib/colors/hexToRgb'; +import { test, expect } from '@playwright/test'; +import { hexToRgb, rgbToHex } from '../src/lib/colors/hexToRgb'; + +test.describe('hexToRgb', () => { + test('it should convert correctly to rgb', () => { + expect(hexToRgb('#000000')).toEqual({ r: 0, g: 0, b: 0 }); + expect(hexToRgb('#ffffff')).toEqual({ r: 255, g: 255, b: 255 }); + expect(hexToRgb('#ff00ff')).toEqual({ r: 255, g: 0, b: 255 }); + expect(hexToRgb('#4488ff')).toEqual({ r: 68, g: 136, b: 255 }); + expect(hexToRgb('#1ab2cd')).toEqual({ r: 26, g: 178, b: 205 }); + expect(hexToRgb('#7f7f7f')).toEqual({ r: 127, g: 127, b: 127 }); + expect(hexToRgb('#0088cc')).toEqual({ r: 0, g: 136, b: 204 }); + expect(hexToRgb('#ffcc00')).toEqual({ r: 255, g: 204, b: 0 }); + expect(hexToRgb('#9900cc')).toEqual({ r: 153, g: 0, b: 204 }); + }); + test("it shouldn't work", () => { + expect(hexToRgb('Hi there')).toEqual(null); + expect(hexToRgb('')).toEqual(null); + expect(hexToRgb('#')).toEqual(null); + expect(hexToRgb('#00000')).toEqual(null); + expect(hexToRgb('#gghh00')).toEqual(null); + expect(hexToRgb('#fffff-')).toEqual(null); + }); +}); + +test.describe('rgbToHex', () => { + test('it works', () => { + expect(rgbToHex(0, 0, 0)).toEqual('#000000'); + expect(rgbToHex(255, 255, 255)).toEqual('#ffffff'); + expect(rgbToHex(255, 0, 255)).toEqual('#ff00ff'); + expect(rgbToHex(42, 42, 42)).toEqual('#2a2a2a'); + }); +}); + +// now real e2e tests +test.describe('the colors page', () => { + test('it should work', async ({ page }) => { + await page.goto('/'); + + await page.getByRole('link', { name: 'Einstellungen ' }).click(); + await page.getByRole('link', { name: ' Farben' }).click(); + + await expect(page).toHaveTitle('Dlool | Farben für Fächer'); + + await page.getByRole('button', { name: ' Neues Fach hinzufügen' }).click(); + + await page.getByPlaceholder('Fach').click(); + await page.getByPlaceholder('Fach').fill('Blau'); + await page.getByRole('button', { name: '#3f51b5' }).click(); + + const sliderLoc = await page.getByRole('slider'); + await expect(await sliderLoc.count()).toEqual(3); + + // set the first two sliders to 0 + await sliderLoc.first().evaluate((el: HTMLInputElement) => (el.value = '0')); + await sliderLoc.nth(1).evaluate((el: HTMLInputElement) => (el.value = '0')); + await sliderLoc.nth(2).evaluate((el: HTMLInputElement) => (el.value = '255')); + (await sliderLoc.all()).forEach(async (el) => { + await el.dispatchEvent('input'); + await el.dispatchEvent('change'); + }); + + const previewArea = await page.locator( + '.p-4 > .bg-\\[rgb\\(var\\(--red\\)\\,var\\(--green\\)\\,var\\(--blue\\)\\)\\]' + ); + const bgValue = await previewArea.evaluate( + (el: HTMLDivElement) => window.getComputedStyle(el).backgroundColor + ); + await expect(bgValue).toEqual('rgb(0, 0, 255)'); + await page.getByRole('button', { name: 'Farbe auswählen' }).click(); + }); + + test('it should be stored on reload', async ({ page }) => { + await page.goto('/settings/colors'); + await expect(page).toHaveTitle('Dlool | Farben für Fächer'); + + await page.getByRole('button', { name: ' Neues Fach hinzufügen' }).click(); + // now fill in the subject + await page.getByPlaceholder('Fach').click(); + await page.getByPlaceholder('Fach').fill('Playwright'); + await page.getByRole('button', { name: '#3f51b5' }).click(); + const sliderList = await page.getByRole('slider').all(); + + for (const el of sliderList) { + await el.evaluate((el: HTMLInputElement) => (el.value = '0')); + await el.dispatchEvent('input'); + await el.dispatchEvent('change'); + } + + await page.getByRole('button', { name: 'Farbe auswählen' }).click(); + + // run two times + for (let i = 0; i < 2; i++) { + await expect(await page.getByRole('button', { name: '#000000' })).toBeVisible(); + await expect(await page.getByPlaceholder('Fach')).toHaveValue('Playwright'); + await page.reload(); + } + }); +});