diff --git a/documentation.json b/documentation.json index 262767a..d687092 100644 --- a/documentation.json +++ b/documentation.json @@ -598,7 +598,7 @@ }, { "name": "ReportFrameComponent", - "id": "component-ReportFrameComponent-e323810cea7fa8a83dc4423d7e4d93fbb78ad0dfd2b31f4f6f0327cd9c1b5582e77db10297dcea5fdc7718f4b7ae0a9c5bdfed47003df780b535753863652249", + "id": "component-ReportFrameComponent-e96f8cf5e81556af6935e5f14db2c7eb7d81c6dcd8253cb410c9793fda620e197a247e0b277c993a75ed0a65a9aa5e2d5f9e67ca08eb2509235d32d48a012ba0", "file": "src/stories/reports/report-frame.component.ts", "changeDetection": "ChangeDetectionStrategy.OnPush", "encapsulation": [], @@ -607,7 +607,7 @@ "inputs": [], "outputs": [], "providers": [], - "selector": "storybook-report-frame", + "selector": "app-report-frame", "styleUrls": [], "styles": [], "templateUrl": [ @@ -701,7 +701,7 @@ "description": "", "rawdescription": "\n", "type": "component", - "sourceCode": "import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';\n\n@Component({\n selector: 'storybook-report-frame',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'report-frame-host',\n },\n templateUrl: './report-frame.component.html',\n styleUrl: './report-frame.component.css',\n})\nexport class ReportFrameComponent {\n readonly title = input.required();\n readonly description = input.required();\n readonly src = input.required();\n\n private readonly sanitizer = inject(DomSanitizer);\n\n readonly safeSrc = computed(() =>\n this.sanitizer.bypassSecurityTrustResourceUrl(this.src()),\n );\n}\n", + "sourceCode": "import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';\n\n@Component({\n selector: 'app-report-frame',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'report-frame-host',\n },\n templateUrl: './report-frame.component.html',\n styleUrl: './report-frame.component.css',\n})\nexport class ReportFrameComponent {\n readonly title = input.required();\n readonly description = input.required();\n readonly src = input.required();\n\n private readonly sanitizer = inject(DomSanitizer);\n\n readonly safeSrc = computed(() =>\n this.sanitizer.bypassSecurityTrustResourceUrl(this.src()),\n );\n}\n", "styleUrl": "./report-frame.component.css", "assetsDirs": [], "styleUrlsData": "", @@ -2021,29 +2021,29 @@ { "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/features/todos/components/todo-filters-panel/todo-filters-panel.test-ids.ts", + "file": "src/app/features/todos/components/todo-item/todo-item.test-ids.ts", "deprecated": false, "deprecationMessage": "", "type": "unknown", - "defaultValue": "generateTestIds({ scopeName: 'filtersPanel' }).add(\n 'searchInput',\n 'filterAll',\n 'filterActive',\n 'filterCompleted',\n)" + "defaultValue": "generateTestIds({ scopeName: 'todoItem' }).add(\n 'checkbox',\n 'title',\n 'meta',\n 'stateButton',\n 'deleteButton',\n)" }, { "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/features/todos/components/todo-summary/todo-summary.test-ids.ts", + "file": "src/app/features/todos/components/todo-filters-panel/todo-filters-panel.test-ids.ts", "deprecated": false, "deprecationMessage": "", "type": "unknown", - "defaultValue": "generateTestIds({\n scopeName: 'todoSummary',\n}).add('totalValue', 'activeValue', 'completedValue')" + "defaultValue": "generateTestIds({ scopeName: 'filtersPanel' }).add(\n 'searchInput',\n 'filterAll',\n 'filterActive',\n 'filterCompleted',\n)" }, { "ctype": "miscellaneous", "subtype": "variable", - "file": "src/app/features/todos/components/todo-item/todo-item.test-ids.ts", + "file": "src/app/features/todos/components/todo-summary/todo-summary.test-ids.ts", "deprecated": false, "deprecationMessage": "", "type": "unknown", - "defaultValue": "generateTestIds({ scopeName: 'todoItem' }).add(\n 'checkbox',\n 'title',\n 'meta',\n 'stateButton',\n 'deleteButton',\n)" + "defaultValue": "generateTestIds({\n scopeName: 'todoSummary',\n}).add('totalValue', 'activeValue', 'completedValue')" }, { "ctype": "miscellaneous", @@ -2070,7 +2070,7 @@ "type": "ScopeName", "deprecated": false, "deprecationMessage": "", - "destrucuredGroupId": "ae0490c6-480d-4136-958b-ff277c8b0711", + "destrucuredGroupId": "0f318d07-9ff8-44d7-96b3-4ce50d9b8c0c", "destructuredParameter": true } ], @@ -2080,7 +2080,7 @@ "type": "ScopeName", "deprecated": false, "deprecationMessage": "", - "destrucuredGroupId": "ae0490c6-480d-4136-958b-ff277c8b0711", + "destrucuredGroupId": "0f318d07-9ff8-44d7-96b3-4ce50d9b8c0c", "destructuredParameter": true, "tagName": { "text": "param" @@ -2432,7 +2432,7 @@ "type": "ScopeName", "deprecated": false, "deprecationMessage": "", - "destrucuredGroupId": "ae0490c6-480d-4136-958b-ff277c8b0711", + "destrucuredGroupId": "0f318d07-9ff8-44d7-96b3-4ce50d9b8c0c", "destructuredParameter": true } ], @@ -2442,7 +2442,7 @@ "type": "ScopeName", "deprecated": false, "deprecationMessage": "", - "destrucuredGroupId": "ae0490c6-480d-4136-958b-ff277c8b0711", + "destrucuredGroupId": "0f318d07-9ff8-44d7-96b3-4ce50d9b8c0c", "destructuredParameter": true, "tagName": { "text": "param" diff --git a/e2e/vrt/stories.spec.ts b/e2e/vrt/stories.spec.ts index 7d6731b..586eb2b 100644 --- a/e2e/vrt/stories.spec.ts +++ b/e2e/vrt/stories.spec.ts @@ -1,6 +1,7 @@ -/// - -import { test, expect } from '@playwright/test'; +import { readdirSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { test, expect, type Page } from '@playwright/test'; +import { fileURLToPath } from 'node:url'; /** * Visual Regression Tests for Storybook Stories @@ -30,20 +31,129 @@ const viewports = [ }, ]; -function getStoryPath(componentPath: string, storyName: string): string { - const kebabCaseStory = storyName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - const id = `${componentPath}--${kebabCaseStory}`; - return `/iframe.html?viewMode=story&id=${id}`; +interface StoryDefinition { + exportName: string; + id: string; + title?: string; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const storyTitlePrefix = 'Todos/Components'; +const storiesSourceRoot = resolve(__dirname, '../../src'); + +function getStoryPath(storyId: string): string { + return `/iframe.html?viewMode=story&id=${storyId}`; } -test.describe('TodoAddForm Component Stories', () => { - const stories = [ - { storyName: 'Empty', description: 'Default empty state' }, - { storyName: 'Filled', description: 'With content' }, - { storyName: 'RequiredError', description: 'Validation error for empty/whitespace' }, - { storyName: 'MaxLengthError', description: 'Validation error for length exceeded' }, - ]; +function collectStoryFiles(directoryPath: string): string[] { + return readdirSync(directoryPath, { withFileTypes: true }).flatMap((entry) => { + const entryPath = resolve(directoryPath, entry.name); + + if (entry.isDirectory()) { + return collectStoryFiles(entryPath); + } + + return entry.name.endsWith('.stories.ts') ? [entryPath] : []; + }); +} + +function toStorySegment(value: string): string { + return value + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/[^a-zA-Z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-') + .toLowerCase(); +} + +function formatStoryName(exportName: string): string { + return exportName.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); +} + +function parseStoriesFromFile(filePath: string): StoryDefinition[] { + const source = readFileSync(filePath, 'utf8'); + const titleMatch = source.match(/title:\s*['"`]([^'"`]+)['"`]/); + const title = titleMatch?.[1]; + + if (!title?.startsWith(storyTitlePrefix)) { + return []; + } + + const titleId = title + .split('/') + .map((segment) => toStorySegment(segment)) + .join('-'); + + return Array.from(source.matchAll(/export const\s+(\w+)\s*:\s*Story\b/g), (match) => ({ + exportName: match[1], + id: `${titleId}--${toStorySegment(match[1])}`, + title, + })); +} + +function discoverStoriesFromSource(): StoryDefinition[] { + const stories = collectStoryFiles(storiesSourceRoot) + .flatMap((filePath) => parseStoriesFromFile(filePath)) + .sort((a, b) => { + const titleCompare = (a.title ?? '').localeCompare(b.title ?? ''); + if (titleCompare !== 0) { + return titleCompare; + } + + return a.exportName.localeCompare(b.exportName); + }); + if (stories.length === 0) { + throw new Error( + `No stories found for title prefix "${storyTitlePrefix}". Check Storybook story titles or story export patterns.` + ); + } + + return stories; +} + +const stories = discoverStoriesFromSource(); + +async function waitForStoryToRender(page: Page, path: string): Promise { + const renderStateHandle = await page.waitForFunction( + () => { + const body = document.body; + if (!body) { + return null; + } + + if (body.classList.contains('sb-show-main')) { + return 'ready'; + } + + if (body.classList.contains('sb-show-nopreview')) { + return 'no-preview'; + } + + if (body.classList.contains('sb-show-errordisplay')) { + const storybookError = document.querySelector('#error-message')?.textContent ?? ''; + return storybookError.trim().length > 0 + ? `storybook-error:${storybookError.trim()}` + : 'storybook-error'; + } + + if (body.children.length === 0 && body.textContent?.trim() === 'Not Found') { + return 'not-found'; + } + + return null; + }, + { timeout: 12_000 } + ); + + const renderState = (await renderStateHandle.jsonValue()) as string; + if (renderState !== 'ready') { + throw new Error(`Story did not render for ${path}. Render state: ${renderState}`); + } +} + +test.describe('Todo Component Stories', () => { for (const viewport of viewports) { test.describe(`@${viewport.name}`, () => { test.use({ @@ -51,47 +161,14 @@ test.describe('TodoAddForm Component Stories', () => { }); for (const story of stories) { - test(`${story.storyName} - ${story.description}`, async ({ page }) => { - const path = getStoryPath('todos-components-todo-add-form', story.storyName); + test(`${story.title ?? 'Unknown'} / ${formatStoryName(story.exportName)}`, async ({ + page, + }) => { + const path = getStoryPath(story.id); await page.goto(path); - await page.waitForLoadState('domcontentloaded'); - const renderStateHandle = await page.waitForFunction( - () => { - const body = document.body; - if (!body) { - return null; - } - - if (body.classList.contains('sb-show-main')) { - return 'ready'; - } - - if (body.classList.contains('sb-show-nopreview')) { - return 'no-preview'; - } - - if (body.classList.contains('sb-show-errordisplay')) { - const storybookError = document.querySelector('#error-message')?.textContent ?? ''; - return storybookError.trim().length > 0 - ? `storybook-error:${storybookError.trim()}` - : 'storybook-error'; - } - - if (body.children.length === 0 && body.textContent?.trim() === 'Not Found') { - return 'not-found'; - } - - return null; - }, - { timeout: 12_000 }, - ); - - const renderState = (await renderStateHandle.jsonValue()) as string; - if (renderState !== 'ready') { - throw new Error(`Story did not render for ${path}. Render state: ${renderState}`); - } + await waitForStoryToRender(page, path); await expect(page).toHaveScreenshot({ fullPage: false, diff --git a/playwright.config.ts b/playwright.config.ts index 5b3e569..69e512b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,8 @@ const isVRTMode = !!process.env['VRT']; const isCI = !!process.env['CI']; const vrtPort = 6106; const vrtHost = '127.0.0.1'; +const ensureStorybookStaticCommand = + '[ -f storybook-static/index.html ] && [ -f storybook-static/iframe.html ] || npx ng run angular-21:build-storybook'; export default defineConfig({ testDir: './e2e', @@ -23,9 +25,7 @@ export default defineConfig({ }, webServer: isVRTMode ? { - command: isCI - ? `npx http-server storybook-static -a ${vrtHost} -p ${vrtPort} -c-1 -s` - : `test -d storybook-static || npx ng run angular-21:build-storybook; npx http-server storybook-static -a ${vrtHost} -p ${vrtPort} -c-1 -s`, + command: `${ensureStorybookStaticCommand}; npx http-server storybook-static -a ${vrtHost} -p ${vrtPort} -c-1 -s`, url: `http://${vrtHost}:${vrtPort}`, reuseExistingServer: false, timeout: 120_000, diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-Empty---Default-empty-state.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Empty.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-Empty---Default-empty-state.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Empty.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-Filled---With-content.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Filled.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-Filled---With-content.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Filled.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-MaxLengthError---Validation-error-for-length-exceeded.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Max-Length-Error.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-MaxLengthError---Validation-error-for-length-exceeded.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Max-Length-Error.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-RequiredError---Validation-error-for-empty-whitespace.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Required-Error.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-desktop-RequiredError---Validation-error-for-empty-whitespace.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Add-Form-Required-Error.png diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-Active-With-Search.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-Active-With-Search.png new file mode 100644 index 0000000..b4fcaa0 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-Active-With-Search.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-All-Items.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-All-Items.png new file mode 100644 index 0000000..d3cdd32 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-All-Items.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png new file mode 100644 index 0000000..fc11c01 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Hero-Completion-Heavy.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Hero-Completion-Heavy.png new file mode 100644 index 0000000..541dd48 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Hero-Completion-Heavy.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Hero-Default.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Hero-Default.png new file mode 100644 index 0000000..279f396 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Hero-Default.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-All-Completed.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-All-Completed.png new file mode 100644 index 0000000..4c82b7c Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-All-Completed.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-Balanced.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-Balanced.png new file mode 100644 index 0000000..e58b736 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-Balanced.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-Empty.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-Empty.png new file mode 100644 index 0000000..8af1d29 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Summary-Empty.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Completed-Only.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Completed-Only.png new file mode 100644 index 0000000..61e75d1 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Completed-Only.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Empty-State.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Empty-State.png new file mode 100644 index 0000000..67c2312 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Empty-State.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Populated.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Populated.png new file mode 100644 index 0000000..e7826e2 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-desktop-Todos-Components-Todo-Task-List-Populated.png differ diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-Empty---Default-empty-state.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Empty.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-Empty---Default-empty-state.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Empty.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-Filled---With-content.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Filled.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-Filled---With-content.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Filled.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-MaxLengthError---Validation-error-for-length-exceeded.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Max-Length-Error.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-MaxLengthError---Validation-error-for-length-exceeded.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Max-Length-Error.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-RequiredError---Validation-error-for-empty-whitespace.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Required-Error.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-mobile-RequiredError---Validation-error-for-empty-whitespace.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Add-Form-Required-Error.png diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-Active-With-Search.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-Active-With-Search.png new file mode 100644 index 0000000..46a1b5e Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-Active-With-Search.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-All-Items.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-All-Items.png new file mode 100644 index 0000000..e03c4fa Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-All-Items.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png new file mode 100644 index 0000000..32c3591 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Hero-Completion-Heavy.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Hero-Completion-Heavy.png new file mode 100644 index 0000000..ffd519c Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Hero-Completion-Heavy.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Hero-Default.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Hero-Default.png new file mode 100644 index 0000000..471e37a Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Hero-Default.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-All-Completed.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-All-Completed.png new file mode 100644 index 0000000..ef70cef Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-All-Completed.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-Balanced.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-Balanced.png new file mode 100644 index 0000000..66ec4fc Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-Balanced.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-Empty.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-Empty.png new file mode 100644 index 0000000..fad52fb Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Summary-Empty.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Completed-Only.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Completed-Only.png new file mode 100644 index 0000000..57781a7 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Completed-Only.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Empty-State.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Empty-State.png new file mode 100644 index 0000000..e40f880 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Empty-State.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Populated.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Populated.png new file mode 100644 index 0000000..e08833f Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-mobile-Todos-Components-Todo-Task-List-Populated.png differ diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-Empty---Default-empty-state.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Empty.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-Empty---Default-empty-state.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Empty.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-Filled---With-content.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Filled.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-Filled---With-content.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Filled.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-MaxLengthError---Validation-error-for-length-exceeded.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Max-Length-Error.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-MaxLengthError---Validation-error-for-length-exceeded.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Max-Length-Error.png diff --git a/snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-RequiredError---Validation-error-for-empty-whitespace.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Required-Error.png similarity index 100% rename from snapshots/vrt/stories.spec.ts/TodoAddForm-Component-Stories-tablet-RequiredError---Validation-error-for-empty-whitespace.png rename to snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Add-Form-Required-Error.png diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-Active-With-Search.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-Active-With-Search.png new file mode 100644 index 0000000..659ce23 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-Active-With-Search.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-All-Items.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-All-Items.png new file mode 100644 index 0000000..d126839 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-All-Items.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png new file mode 100644 index 0000000..5a7b9ef Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Filters-Panel-Completed-With-Search.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Hero-Completion-Heavy.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Hero-Completion-Heavy.png new file mode 100644 index 0000000..3b84163 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Hero-Completion-Heavy.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Hero-Default.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Hero-Default.png new file mode 100644 index 0000000..e30d6f2 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Hero-Default.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-All-Completed.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-All-Completed.png new file mode 100644 index 0000000..d6a09c4 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-All-Completed.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-Balanced.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-Balanced.png new file mode 100644 index 0000000..67328c3 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-Balanced.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-Empty.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-Empty.png new file mode 100644 index 0000000..113a8cc Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Summary-Empty.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Completed-Only.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Completed-Only.png new file mode 100644 index 0000000..db5c7aa Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Completed-Only.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Empty-State.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Empty-State.png new file mode 100644 index 0000000..9929c66 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Empty-State.png differ diff --git a/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Populated.png b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Populated.png new file mode 100644 index 0000000..b5084f8 Binary files /dev/null and b/snapshots/vrt/stories.spec.ts/Todo-Component-Stories-tablet-Todos-Components-Todo-Task-List-Populated.png differ diff --git a/src/app/features/todos/components/todo-filters-panel/todo-filters-panel.component.stories.ts b/src/app/features/todos/components/todo-filters-panel/todo-filters-panel.component.stories.ts new file mode 100644 index 0000000..328c8a9 --- /dev/null +++ b/src/app/features/todos/components/todo-filters-panel/todo-filters-panel.component.stories.ts @@ -0,0 +1,104 @@ +import { FormControl } from '@angular/forms'; +import type { Meta, StoryObj } from '@storybook/angular'; + +import type { TodoFilter } from '../../todo.model'; +import { TodoFiltersPanelComponent } from './todo-filters-panel.component'; + +interface TodoFiltersPanelStoryProps { + searchControl: FormControl; + selectedFilter: TodoFilter; +} + +function createProps( + selectedFilter: TodoFilter = 'all', + searchValue = '' +): TodoFiltersPanelStoryProps { + return { + selectedFilter, + searchControl: new FormControl(searchValue, { nonNullable: true }), + }; +} + +const meta: Meta = { + title: 'Todos/Components/Todo Filters Panel', + component: TodoFiltersPanelComponent, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Reusable filter controls for search and status selection. It emits filter intent while keeping the filter state owned by the container component.', + }, + }, + }, + argTypes: { + filterChanged: { + action: 'filterChanged', + description: 'Emits when the user changes the status filter.', + table: { + category: 'Outputs', + type: { summary: 'string | null' }, + }, + }, + searchControl: { + control: false, + description: 'Reactive form control used for the search text input.', + table: { + category: 'Inputs', + type: { summary: 'FormControl' }, + }, + }, + selectedFilter: { + control: false, + description: 'Currently selected status filter.', + table: { + category: 'Inputs', + type: { summary: "'all' | 'active' | 'completed'" }, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const AllItems: Story = { + render: () => ({ + props: createProps('all'), + }), + parameters: { + docs: { + description: { + story: 'Default state with no search text and the All status selected.', + }, + }, + }, +}; + +export const ActiveWithSearch: Story = { + render: () => ({ + props: createProps('active', 'review'), + }), + parameters: { + docs: { + description: { + story: 'Active filter selected with a populated search query.', + }, + }, + }, +}; + +export const CompletedWithSearch: Story = { + render: () => ({ + props: createProps('completed', 'notes'), + }), + parameters: { + docs: { + description: { + story: 'Completed filter selected to preview combined filter controls state.', + }, + }, + }, +}; diff --git a/src/app/features/todos/components/todo-hero/todo-hero.component.stories.ts b/src/app/features/todos/components/todo-hero/todo-hero.component.stories.ts new file mode 100644 index 0000000..168a131 --- /dev/null +++ b/src/app/features/todos/components/todo-hero/todo-hero.component.stories.ts @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { TodoHeroComponent } from './todo-hero.component'; + +const meta: Meta = { + title: 'Todos/Components/Todo Hero', + component: TodoHeroComponent, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Hero shell for the Todos page. It presents heading content, summary metrics, and projected content for workspace controls.', + }, + }, + }, + argTypes: { + total: { + control: { type: 'number', min: 0 }, + description: 'Total number of todo items.', + table: { + category: 'Inputs', + type: { summary: 'number' }, + }, + }, + active: { + control: { type: 'number', min: 0 }, + description: 'Count of active (not completed) todo items.', + table: { + category: 'Inputs', + type: { summary: 'number' }, + }, + }, + completed: { + control: { type: 'number', min: 0 }, + description: 'Count of completed todo items.', + table: { + category: 'Inputs', + type: { summary: 'number' }, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + }), + args: { + total: 12, + active: 7, + completed: 5, + }, + parameters: { + docs: { + description: { + story: 'Default hero composition with projected workspace panel content.', + }, + }, + }, +}; + +export const CompletionHeavy: Story = { + render: (args) => ({ + props: args, + }), + args: { + total: 18, + active: 3, + completed: 15, + }, + parameters: { + docs: { + description: { + story: 'Hero metrics for a near-complete list with most work closed out.', + }, + }, + }, +}; diff --git a/src/app/features/todos/components/todo-item/todo-item.component.stories.ts b/src/app/features/todos/components/todo-item/todo-item.component.stories.ts new file mode 100644 index 0000000..72be094 --- /dev/null +++ b/src/app/features/todos/components/todo-item/todo-item.component.stories.ts @@ -0,0 +1,113 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import type { TodoItem } from '../../todo.model'; +import { TodoItemComponent } from './todo-item.component'; + +function createTodo(overrides: Partial = {}): TodoItem { + return { + id: 'todo-01', + title: 'Review route-level accessibility checks', + completed: false, + createdAt: new Date('2026-03-01T09:00:00.000Z'), + ...overrides, + }; +} + +const meta: Meta = { + title: 'Todos/Components/Todo Item', + component: TodoItemComponent, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Single task row with completion toggles, relative creation timestamp, and delete action. The component emits intent events for parent-owned state updates.', + }, + }, + }, + argTypes: { + toggleRequested: { + action: 'toggleRequested', + description: 'Emits the todo id when the completion state is toggled.', + table: { + category: 'Outputs', + type: { summary: 'string' }, + }, + }, + deleteRequested: { + action: 'deleteRequested', + description: 'Emits the todo id when the delete action is requested.', + table: { + category: 'Outputs', + type: { summary: 'string' }, + }, + }, + todo: { + control: false, + description: 'Todo item data rendered by the row.', + table: { + category: 'Inputs', + type: { summary: 'TodoItem' }, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Active: Story = { + render: () => ({ + props: { + todo: createTodo(), + }, + }), + parameters: { + docs: { + description: { + story: 'Default active task row with unchecked completion controls.', + }, + }, + }, +}; + +export const Completed: Story = { + render: () => ({ + props: { + todo: createTodo({ + id: 'todo-02', + title: 'Document Storybook coverage', + completed: true, + createdAt: new Date('2026-02-25T16:30:00.000Z'), + }), + }, + }), + parameters: { + docs: { + description: { + story: 'Completed variant showing reopened action labels and completed styling.', + }, + }, + }, +}; + +export const LongTitle: Story = { + render: () => ({ + props: { + todo: createTodo({ + id: 'todo-03', + title: + 'Prepare a concise release retrospective and highlight UX outcomes from accessibility and Storybook improvements', + }), + }, + }), + parameters: { + docs: { + description: { + story: 'Stress case for long task text wrapping and action alignment.', + }, + }, + }, +}; diff --git a/src/app/features/todos/components/todo-summary/todo-summary.component.stories.ts b/src/app/features/todos/components/todo-summary/todo-summary.component.stories.ts new file mode 100644 index 0000000..779e613 --- /dev/null +++ b/src/app/features/todos/components/todo-summary/todo-summary.component.stories.ts @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { TodoSummaryComponent } from './todo-summary.component'; + +const meta: Meta = { + title: 'Todos/Components/Todo Summary', + component: TodoSummaryComponent, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Compact summary rail that displays total, active, and completed task counts for the current workspace state.', + }, + }, + }, + args: { + total: 12, + active: 7, + completed: 5, + }, + argTypes: { + total: { + control: { type: 'number', min: 0 }, + description: 'Total todo count.', + table: { + category: 'Inputs', + type: { summary: 'number' }, + }, + }, + active: { + control: { type: 'number', min: 0 }, + description: 'Active todo count.', + table: { + category: 'Inputs', + type: { summary: 'number' }, + }, + }, + completed: { + control: { type: 'number', min: 0 }, + description: 'Completed todo count.', + table: { + category: 'Inputs', + type: { summary: 'number' }, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Balanced: Story = { + parameters: { + docs: { + description: { + story: 'Representative state with a mix of active and completed work.', + }, + }, + }, +}; + +export const AllCompleted: Story = { + args: { + total: 9, + active: 0, + completed: 9, + }, + parameters: { + docs: { + description: { + story: 'State after all items in the current scope are completed.', + }, + }, + }, +}; + +export const Empty: Story = { + args: { + total: 0, + active: 0, + completed: 0, + }, + parameters: { + docs: { + description: { + story: 'Initial state before any todos are created.', + }, + }, + }, +}; diff --git a/src/app/features/todos/components/todo-task-list/todo-task-list.component.stories.ts b/src/app/features/todos/components/todo-task-list/todo-task-list.component.stories.ts new file mode 100644 index 0000000..b192276 --- /dev/null +++ b/src/app/features/todos/components/todo-task-list/todo-task-list.component.stories.ts @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import type { TodoItem } from '../../todo.model'; +import { TodoTaskListComponent } from './todo-task-list.component'; + +function createTodo(id: string, title: string, completed: boolean, createdAtIso: string): TodoItem { + return { + id, + title, + completed, + createdAt: new Date(createdAtIso), + }; +} + +const sampleTodos: TodoItem[] = [ + createTodo( + 'todo-01', + 'Ship Storybook stories for todo components', + false, + '2026-03-01T09:00:00.000Z' + ), + createTodo('todo-02', 'Review accessibility checklist updates', true, '2026-02-27T12:15:00.000Z'), + createTodo( + 'todo-03', + 'Refine onboarding copy for filters panel', + false, + '2026-03-03T07:45:00.000Z' + ), +]; + +const meta: Meta = { + title: 'Todos/Components/Todo Task List', + component: TodoTaskListComponent, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'List renderer for todo items. It displays an empty state when no tasks are present and forwards item-level toggle/delete intents to the parent container.', + }, + }, + }, + argTypes: { + toggleRequested: { + action: 'toggleRequested', + description: 'Emits todo id when a child item requests completion toggle.', + table: { + category: 'Outputs', + type: { summary: 'string' }, + }, + }, + deleteRequested: { + action: 'deleteRequested', + description: 'Emits todo id when a child item requests deletion.', + table: { + category: 'Outputs', + type: { summary: 'string' }, + }, + }, + todos: { + control: false, + description: 'Ordered set of todos rendered into list items.', + table: { + category: 'Inputs', + type: { summary: 'TodoItem[]' }, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Populated: Story = { + render: () => ({ + props: { + todos: sampleTodos, + }, + }), + parameters: { + docs: { + description: { + story: 'List with mixed active and completed items.', + }, + }, + }, +}; + +export const EmptyState: Story = { + render: () => ({ + props: { + todos: [], + }, + }), + parameters: { + docs: { + description: { + story: 'Fallback UI displayed when no items match the current view.', + }, + }, + }, +}; + +export const CompletedOnly: Story = { + render: () => ({ + props: { + todos: sampleTodos.filter((todo) => todo.completed), + }, + }), + parameters: { + docs: { + description: { + story: 'Filtered list where every item is completed.', + }, + }, + }, +}; diff --git a/tsconfig.json b/tsconfig.json index e086d96..ee7dfc5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,9 @@ }, { "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.playwright.json" } ] }