From 6e701d61b3033e1f36f0002072d506c6b29385f7 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 1 Apr 2026 11:05:45 +0200 Subject: [PATCH] Adds playwright component fixture tests --- .github/workflows/component-fixture-tests.yml | 58 +++++++++ eslint.config.js | 8 ++ .../imageCarousel.fixture.ts | 13 ++ test/componentFixtures/playwright/.gitignore | 5 + .../playwright/package-lock.json | 112 ++++++++++++++++ .../componentFixtures/playwright/package.json | 17 +++ .../playwright/playwright.config.ts | 27 ++++ .../playwright/tests/imageCarousel.spec.ts | 123 ++++++++++++++++++ .../playwright/tests/utils.ts | 24 ++++ .../playwright/tsconfig.json | 18 +++ 10 files changed, 405 insertions(+) create mode 100644 .github/workflows/component-fixture-tests.yml create mode 100644 test/componentFixtures/playwright/.gitignore create mode 100644 test/componentFixtures/playwright/package-lock.json create mode 100644 test/componentFixtures/playwright/package.json create mode 100644 test/componentFixtures/playwright/playwright.config.ts create mode 100644 test/componentFixtures/playwright/tests/imageCarousel.spec.ts create mode 100644 test/componentFixtures/playwright/tests/utils.ts create mode 100644 test/componentFixtures/playwright/tsconfig.json diff --git a/.github/workflows/component-fixture-tests.yml b/.github/workflows/component-fixture-tests.yml new file mode 100644 index 00000000000..c2d56d0c206 --- /dev/null +++ b/.github/workflows/component-fixture-tests.yml @@ -0,0 +1,58 @@ +name: Component Fixture Tests + +on: + push: + branches: [main] + pull_request: + branches: + - main + - 'release/*' + +permissions: + contents: read + +concurrency: + group: component-fixture-tests-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + component-fixture-tests: + name: Component Fixture Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install dependencies + run: npm ci + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Transpile source + run: npm run transpile-client + + - name: Install Playwright test dependencies + run: npm ci + working-directory: test/componentFixtures/playwright + + - name: Install Playwright Chromium + run: npx playwright install chromium + working-directory: test/componentFixtures/playwright + + - name: Run Playwright tests + run: npx playwright test + working-directory: test/componentFixtures/playwright + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v7 + with: + name: playwright-test-results + path: test/componentFixtures/playwright/test-results/ diff --git a/eslint.config.js b/eslint.config.js index df163b7a06d..4f47cb3f112 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2223,6 +2223,14 @@ export default tseslint.config( '@modelcontextprotocol/sdk/**/*', '*' // node modules ] + }, + { + 'target': 'test/componentFixtures/playwright/**', + 'restrictions': [ + 'test/componentFixtures/playwright/**', + '@playwright/*', + '*' // node modules + ] } ] } diff --git a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts index 613a14c5ae0..88dc3ac98f9 100644 --- a/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/imageCarousel.fixture.ts @@ -14,6 +14,12 @@ import { ImageCarouselEditorInput } from '../../../contrib/imageCarousel/browser import { ICarouselImage, IImageCarouselCollection } from '../../../contrib/imageCarousel/browser/imageCarouselTypes.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; import '../../../contrib/imageCarousel/browser/media/imageCarousel.css'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { NullFileSystemProvider } from '../../../../platform/files/test/common/nullFileSystemProvider.js'; +import { FileService } from '../../../../platform/files/common/fileService.js'; +import { NullLogService } from '../../../../platform/log/common/log.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { IWebviewService } from '../../../contrib/webview/browser/webview.js'; function createSolidPng(r: number, g: number, b: number, width: number = 64, height: number = 64): VSBuffer { const canvas = mainWindow.document.createElement('canvas'); @@ -52,6 +58,13 @@ async function renderCarousel(context: ComponentFixtureContext, collection: IIma const instantiationService = createEditorServices(disposableStore, { colorTheme: theme, + additionalServices: ({ defineInstance }) => { + const fileService = new FileService(new NullLogService()); + fileService.registerProvider(Schemas.file, new NullFileSystemProvider()); + disposableStore.add(fileService); + defineInstance(IFileService, fileService); + defineInstance(IWebviewService, new class extends mock() { }()); + }, }); const editor = disposableStore.add( diff --git a/test/componentFixtures/playwright/.gitignore b/test/componentFixtures/playwright/.gitignore new file mode 100644 index 00000000000..f93d5c26191 --- /dev/null +++ b/test/componentFixtures/playwright/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +out/ +test-results/ +playwright-report/ +blob-report/ diff --git a/test/componentFixtures/playwright/package-lock.json b/test/componentFixtures/playwright/package-lock.json new file mode 100644 index 00000000000..0e6286fa07a --- /dev/null +++ b/test/componentFixtures/playwright/package-lock.json @@ -0,0 +1,112 @@ +{ + "name": "code-oss-component-fixture-tests", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-oss-component-fixture-tests", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "22.x", + "typescript": "^5.8.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/componentFixtures/playwright/package.json b/test/componentFixtures/playwright/package.json new file mode 100644 index 00000000000..dfd817becec --- /dev/null +++ b/test/componentFixtures/playwright/package.json @@ -0,0 +1,17 @@ +{ + "name": "code-oss-component-fixture-tests", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "22.x", + "typescript": "^5.8.0" + } +} diff --git a/test/componentFixtures/playwright/playwright.config.ts b/test/componentFixtures/playwright/playwright.config.ts new file mode 100644 index 00000000000..3a7aa9e49f8 --- /dev/null +++ b/test/componentFixtures/playwright/playwright.config.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { defineConfig } from '@playwright/test'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + testDir: './tests', + timeout: 30_000, + retries: 0, + use: { + trace: 'on-first-retry', + }, + webServer: { + command: 'npx component-explorer serve -p ../component-explorer.json --background --attach -vv', + cwd: __dirname, + wait: { + stdout: /current: http:\/\/localhost:(?\d+)\/___explorer/, + }, + timeout: 120_000, + }, +}); diff --git a/test/componentFixtures/playwright/tests/imageCarousel.spec.ts b/test/componentFixtures/playwright/tests/imageCarousel.spec.ts new file mode 100644 index 00000000000..22da5824a4f --- /dev/null +++ b/test/componentFixtures/playwright/tests/imageCarousel.spec.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { test, expect } from '@playwright/test'; +import { openFixture } from './utils.js'; + +test.describe('Image Carousel', () => { + + test('clicking next arrow advances to the next image', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSection/Dark'); + + const counter = page.locator('.image-counter'); + await expect(counter).toHaveText('1 / 5'); + + const nextBtn = page.locator('button.next-arrow'); + await nextBtn.click(); + + await expect(counter).toHaveText('2 / 5'); + }); + + test('clicking previous arrow goes back', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSectionMiddleImage/Dark'); + + const counter = page.locator('.image-counter'); + // Starts at image 3 (index 2) + await expect(counter).toHaveText('3 / 5'); + + const prevBtn = page.locator('button.prev-arrow'); + await prevBtn.click(); + + await expect(counter).toHaveText('2 / 5'); + }); + + test('previous button is disabled on first image', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSection/Dark'); + + const prevBtn = page.locator('button.prev-arrow'); + await expect(prevBtn).toBeDisabled(); + + const nextBtn = page.locator('button.next-arrow'); + await expect(nextBtn).toBeEnabled(); + }); + + test('next button is disabled on last image', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSection/Dark'); + + const nextBtn = page.locator('button.next-arrow'); + + // Click through to the last image (5 images, need 4 clicks) + for (let i = 0; i < 4; i++) { + await nextBtn.click(); + } + + await expect(nextBtn).toBeDisabled(); + + const counter = page.locator('.image-counter'); + await expect(counter).toHaveText('5 / 5'); + }); + + test('caption updates when navigating', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSection/Dark'); + + const caption = page.locator('.caption-text'); + // First image: "A red image" + await expect(caption).toHaveText('A red image'); + + await page.locator('button.next-arrow').click(); + // Second image: "A green image" + await expect(caption).toHaveText('A green image'); + + await page.locator('button.next-arrow').click(); + // Third image has no caption — element should be hidden + await expect(caption).toBeHidden(); + }); + + test('clicking a thumbnail selects that image', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSection/Dark'); + + const thumbnails = page.locator('button.thumbnail'); + const counter = page.locator('.image-counter'); + + // Click the third thumbnail + await thumbnails.nth(2).click(); + await expect(counter).toHaveText('3 / 5'); + + // The clicked thumbnail should be active + await expect(thumbnails.nth(2)).toHaveClass(/active/); + }); + + test('keyboard left/right arrow navigation works', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleSection/Dark'); + + const counter = page.locator('.image-counter'); + await expect(counter).toHaveText('1 / 5'); + + // Focus the slideshow container for keyboard events + await page.locator('.slideshow-container').focus(); + + await page.keyboard.press('ArrowRight'); + await expect(counter).toHaveText('2 / 5'); + + await page.keyboard.press('ArrowRight'); + await expect(counter).toHaveText('3 / 5'); + + await page.keyboard.press('ArrowLeft'); + await expect(counter).toHaveText('2 / 5'); + }); + + test('single image carousel disables both nav buttons', async ({ page }) => { + await openFixture(page, 'imageCarousel/imageCarousel/SingleImage/Dark'); + + const prevBtn = page.locator('button.prev-arrow'); + const nextBtn = page.locator('button.next-arrow'); + + await expect(prevBtn).toBeDisabled(); + await expect(nextBtn).toBeDisabled(); + + const counter = page.locator('.image-counter'); + await expect(counter).toHaveText('1 / 1'); + }); +}); diff --git a/test/componentFixtures/playwright/tests/utils.ts b/test/componentFixtures/playwright/tests/utils.ts new file mode 100644 index 00000000000..2fbbdb78dd0 --- /dev/null +++ b/test/componentFixtures/playwright/tests/utils.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Page } from '@playwright/test'; + +function getBaseURL(): string { + const port = process.env['COMPONENT_EXPLORER_PORT']; + if (!port) { + throw new Error('COMPONENT_EXPLORER_PORT is not set. Is the webServer running?'); + } + return `http://localhost:${port}`; +} + +/** + * Navigates to a component fixture in embedded mode and waits for it to render. + * @param waitForSelector - A CSS selector to wait for after navigation, indicating the fixture has rendered. + */ +export async function openFixture(page: Page, fixtureId: string, waitForSelector = '.image-carousel-editor'): Promise { + const url = `${getBaseURL()}/___explorer?mode=embedded&fixture=${encodeURIComponent(fixtureId)}`; + await page.goto(url, { waitUntil: 'load' }); + await page.locator(waitForSelector).waitFor({ state: 'visible', timeout: 20_000 }); +} diff --git a/test/componentFixtures/playwright/tsconfig.json b/test/componentFixtures/playwright/tsconfig.json new file mode 100644 index 00000000000..d3e10748527 --- /dev/null +++ b/test/componentFixtures/playwright/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "bundler", + "target": "ES2022", + "strict": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "rootDir": ".", + "outDir": "out", + "sourceMap": true, + "skipLibCheck": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"] + }, + "include": ["tests/**/*.ts", "playwright.config.ts"], + "exclude": ["node_modules"] +}