From 9e72229efa226ccb111f3132538c035b201ac925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 15 Aug 2024 11:58:49 +0300 Subject: [PATCH] test: add `test/ui` --- .github/workflows/ci.yaml | 3 + .gitignore | 2 + .npmrc | 1 + package.json | 5 +- pnpm-lock.yaml | 3 + test/ui/README.md | 42 +++++++++++++ test/ui/astro.config.ts | 23 ++++++++ test/ui/package.json | 32 ++++++++++ test/ui/playwright.config.ts | 17 ++++++ test/ui/public/logo.svg | 1 + test/ui/src/content/config.ts | 9 +++ test/ui/src/content/tutorial/meta.md | 5 ++ .../lesson-and-solution/_files/example.html | 8 +++ .../lesson-and-solution/_files/example.js | 1 + .../_solution/example.html | 8 +++ .../lesson-and-solution/_solution/example.js | 1 + .../file-tree/lesson-and-solution/content.md | 6 ++ .../content/tutorial/tests/file-tree/meta.md | 4 ++ .../file-tree/no-solution/_files/example.html | 8 +++ .../file-tree/no-solution/_files/example.js | 1 + .../tests/file-tree/no-solution/content.md | 6 ++ test/ui/src/content/tutorial/tests/meta.md | 4 ++ .../content/tutorial/tests/navigation/meta.md | 10 ++++ .../tests/navigation/page-one/content.md | 6 ++ .../tests/navigation/page-three/content.md | 6 ++ .../tests/navigation/page-two/content.md | 6 ++ .../content/tutorial/tests/preview/meta.md | 5 ++ .../tests/preview/multiple/content.md | 9 +++ .../tutorial/tests/preview/single/content.md | 8 +++ test/ui/src/env.d.ts | 3 + test/ui/src/templates/default/about.html | 14 +++++ test/ui/src/templates/default/index.html | 14 +++++ test/ui/src/templates/default/index.mjs | 23 ++++++++ test/ui/src/templates/default/package.json | 9 +++ test/ui/test/file-tree.test.ts | 59 +++++++++++++++++++ test/ui/test/navigation.test.ts | 30 ++++++++++ test/ui/test/preview.test.ts | 27 +++++++++ test/ui/test/utils.ts | 33 +++++++++++ test/ui/uno.config.ts | 47 +++++++++++++++ 39 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 .npmrc create mode 100644 test/ui/README.md create mode 100644 test/ui/astro.config.ts create mode 100644 test/ui/package.json create mode 100644 test/ui/playwright.config.ts create mode 100644 test/ui/public/logo.svg create mode 100644 test/ui/src/content/config.ts create mode 100644 test/ui/src/content/tutorial/meta.md create mode 100644 test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.html create mode 100644 test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.js create mode 100644 test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.html create mode 100644 test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.js create mode 100644 test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/content.md create mode 100644 test/ui/src/content/tutorial/tests/file-tree/meta.md create mode 100644 test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.html create mode 100644 test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.js create mode 100644 test/ui/src/content/tutorial/tests/file-tree/no-solution/content.md create mode 100644 test/ui/src/content/tutorial/tests/meta.md create mode 100644 test/ui/src/content/tutorial/tests/navigation/meta.md create mode 100644 test/ui/src/content/tutorial/tests/navigation/page-one/content.md create mode 100644 test/ui/src/content/tutorial/tests/navigation/page-three/content.md create mode 100644 test/ui/src/content/tutorial/tests/navigation/page-two/content.md create mode 100644 test/ui/src/content/tutorial/tests/preview/meta.md create mode 100644 test/ui/src/content/tutorial/tests/preview/multiple/content.md create mode 100644 test/ui/src/content/tutorial/tests/preview/single/content.md create mode 100644 test/ui/src/env.d.ts create mode 100644 test/ui/src/templates/default/about.html create mode 100644 test/ui/src/templates/default/index.html create mode 100644 test/ui/src/templates/default/index.mjs create mode 100644 test/ui/src/templates/default/package.json create mode 100644 test/ui/test/file-tree.test.ts create mode 100644 test/ui/test/navigation.test.ts create mode 100644 test/ui/test/preview.test.ts create mode 100644 test/ui/test/utils.ts create mode 100644 test/ui/uno.config.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c00ef21b..906958dc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,6 +32,9 @@ jobs: - name: Lint run: pnpm lint + - name: Install Playwright Dependencies + run: pnpm exec playwright install chromium --with-deps + - name: Test run: pnpm test diff --git a/.gitignore b/.gitignore index 592c9c08..8cf039c0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo .tmp .tmp-* +/test/**/test-results +/test/**/.astro diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..3bd3b7de --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shell-emulator=true diff --git a/package.json b/package.json index 049bfb39..5bfafe2d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "build": "pnpm run --stream --filter=@tutorialkit/* --filter=create-tutorial build", + "build": "pnpm run --stream --filter='@tutorialkit/*' --filter=create-tutorial build", "dev": "TUTORIALKIT_DEV=true pnpm -r --parallel --stream --filter='./packages/**' run dev", "changelog": "./scripts/changelog.mjs", "clean": "./scripts/clean.sh", @@ -16,7 +16,7 @@ "demo": "pnpm run --filter=demo.tutorialkit.dev dev", "demo:build": "pnpm run build && pnpm run --filter=demo.tutorialkit.dev build", "lint": "eslint \"{packages,docs,extensions,integration}/**/*\"", - "test": "pnpm run --stream --filter=@tutorialkit/* test --run" + "test": "CI=true pnpm run --stream --filter='@tutorialkit/*' test" }, "license": "MIT", "packageManager": "pnpm@8.15.6", @@ -30,6 +30,7 @@ "eslint-plugin-astro": "^1.2.3", "husky": "^9.0.11", "is-ci": "^3.0.1", + "playwright": "^1.46.0", "prettier": "^3.3.2", "prettier-plugin-astro": "^0.14.1", "tempfile": "^5.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4b9da9f..cd926578 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: is-ci: specifier: ^3.0.1 version: 3.0.1 + playwright: + specifier: ^1.46.0 + version: 1.46.0 prettier: specifier: ^3.3.2 version: 3.3.2 diff --git a/test/ui/README.md b/test/ui/README.md new file mode 100644 index 00000000..b103c7be --- /dev/null +++ b/test/ui/README.md @@ -0,0 +1,42 @@ +# UI Tests + +> Tests for verifying TutorialKit works as expected in the browser. Tests are run against locally linked `@tutorialkit` packages. + +## Running + +- `pnpm exec playwright install chromium --with-deps` - When running the tests first time +- `pnpm test` + +## Development + +- `pnpm start` - Starts example/fixture project's development server +- `pnpm test:ui` - Start Playwright in UI mode + +## Structure + +Test cases are located in `test` directory. +Each test file has its own `chapter`, that contains `lesson`s for test cases: + +For example Navigation tests: + +``` +├── src/content/tutorial +│ └── tests +│ └──── navigation +│ ├── page-one +│ ├── page-three +│ └── page-two +└── test + └── navigation.test.ts +``` + +Or File Tree tests: + +``` +├── src/content/tutorial +│ └── tests +│ └── file-tree +│ └── lesson-and-solution +└── test + └── file-tree.test.ts +``` diff --git a/test/ui/astro.config.ts b/test/ui/astro.config.ts new file mode 100644 index 00000000..6814ced5 --- /dev/null +++ b/test/ui/astro.config.ts @@ -0,0 +1,23 @@ +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; +import tutorialkit from '@tutorialkit/astro'; +import { defineConfig } from 'astro/config'; + +const require = createRequire(import.meta.url); +const astroDist = resolve(require.resolve('astro/package.json'), '..'); +const swapFunctionEntry = resolve(astroDist, 'dist/transitions/swap-functions.js'); + +export default defineConfig({ + devToolbar: { enabled: false }, + server: { port: 4329 }, + integrations: [tutorialkit()], + + vite: { + resolve: { + alias: { + // work-around for https://github.com/stackblitz/tutorialkit/pull/238 + 'node_modules/astro/dist/transitions/swap-functions': swapFunctionEntry, + }, + }, + }, +}); diff --git a/test/ui/package.json b/test/ui/package.json new file mode 100644 index 00000000..07607c9c --- /dev/null +++ b/test/ui/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tutorialkit/test-ui", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "preview": "astro build && astro preview", + "test": "playwright test", + "test:ui": "pnpm run test --ui" + }, + "devDependencies": { + "@astrojs/react": "^3.6.0", + "@iconify-json/ph": "^1.1.13", + "@iconify-json/svg-spinners": "^1.1.2", + "@playwright/test": "^1.46.0", + "@tutorialkit/astro": "workspace:*", + "@tutorialkit/components-react": "workspace:*", + "@tutorialkit/runtime": "workspace:*", + "@tutorialkit/theme": "workspace:*", + "@tutorialkit/types": "workspace:*", + "@types/node": "^22.2.0", + "@unocss/reset": "^0.59.4", + "@unocss/transformer-directives": "^0.62.0", + "astro": "^4.12.0", + "fast-glob": "^3.3.2", + "playwright": "^1.46.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "unocss": "^0.59.4" + } +} diff --git a/test/ui/playwright.config.ts b/test/ui/playwright.config.ts new file mode 100644 index 00000000..3c09769f --- /dev/null +++ b/test/ui/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + expect: { + timeout: process.env.CI ? 30_000 : 10_000, + }, + use: { + baseURL: 'http://localhost:4329', + }, + webServer: { + command: 'pnpm preview', + url: 'http://localhost:4329', + reuseExistingServer: !process.env.CI, + stdout: 'ignore', + stderr: 'pipe', + }, +}); diff --git a/test/ui/public/logo.svg b/test/ui/public/logo.svg new file mode 100644 index 00000000..57ad62a4 --- /dev/null +++ b/test/ui/public/logo.svg @@ -0,0 +1 @@ + diff --git a/test/ui/src/content/config.ts b/test/ui/src/content/config.ts new file mode 100644 index 00000000..8e595c05 --- /dev/null +++ b/test/ui/src/content/config.ts @@ -0,0 +1,9 @@ +import { contentSchema } from '@tutorialkit/types'; +import { defineCollection } from 'astro:content'; + +const tutorial = defineCollection({ + type: 'content', + schema: contentSchema, +}); + +export const collections = { tutorial }; diff --git a/test/ui/src/content/tutorial/meta.md b/test/ui/src/content/tutorial/meta.md new file mode 100644 index 00000000..29eef72c --- /dev/null +++ b/test/ui/src/content/tutorial/meta.md @@ -0,0 +1,5 @@ +--- +type: tutorial +mainCommand: '' +prepareCommands: [] +--- diff --git a/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.html b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.html new file mode 100644 index 00000000..011dc537 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.html @@ -0,0 +1,8 @@ + + + Lesson file example.html title + + + Lesson file example.html content + + diff --git a/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.js b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.js new file mode 100644 index 00000000..cd356077 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.js @@ -0,0 +1 @@ +export default 'Lesson file example.js content'; diff --git a/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.html b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.html new file mode 100644 index 00000000..14493277 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.html @@ -0,0 +1,8 @@ + + + Solution file example.html title + + + Solution file example.html content + + diff --git a/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.js b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.js new file mode 100644 index 00000000..6cf75a45 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.js @@ -0,0 +1 @@ +export default 'Solution file example.js content'; diff --git a/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/content.md b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/content.md new file mode 100644 index 00000000..8a53098b --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/lesson-and-solution/content.md @@ -0,0 +1,6 @@ +--- +type: lesson +title: Lesson and solution +--- + +# File Tree test - Lesson and solution diff --git a/test/ui/src/content/tutorial/tests/file-tree/meta.md b/test/ui/src/content/tutorial/tests/file-tree/meta.md new file mode 100644 index 00000000..0c250534 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/meta.md @@ -0,0 +1,4 @@ +--- +type: chapter +title: File Tree +--- diff --git a/test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.html b/test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.html new file mode 100644 index 00000000..011dc537 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.html @@ -0,0 +1,8 @@ + + + Lesson file example.html title + + + Lesson file example.html content + + diff --git a/test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.js b/test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.js new file mode 100644 index 00000000..cd356077 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/no-solution/_files/example.js @@ -0,0 +1 @@ +export default 'Lesson file example.js content'; diff --git a/test/ui/src/content/tutorial/tests/file-tree/no-solution/content.md b/test/ui/src/content/tutorial/tests/file-tree/no-solution/content.md new file mode 100644 index 00000000..54342155 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/file-tree/no-solution/content.md @@ -0,0 +1,6 @@ +--- +type: lesson +title: No solution +--- + +# File Tree test - No solution diff --git a/test/ui/src/content/tutorial/tests/meta.md b/test/ui/src/content/tutorial/tests/meta.md new file mode 100644 index 00000000..a9cbeaef --- /dev/null +++ b/test/ui/src/content/tutorial/tests/meta.md @@ -0,0 +1,4 @@ +--- +type: part +title: Tests +--- diff --git a/test/ui/src/content/tutorial/tests/navigation/meta.md b/test/ui/src/content/tutorial/tests/navigation/meta.md new file mode 100644 index 00000000..4842a08d --- /dev/null +++ b/test/ui/src/content/tutorial/tests/navigation/meta.md @@ -0,0 +1,10 @@ +--- +type: chapter +title: Navigation +lessons: + - page-one + - page-two + - page-three +mainCommand: '' +prepareCommands: [] +--- diff --git a/test/ui/src/content/tutorial/tests/navigation/page-one/content.md b/test/ui/src/content/tutorial/tests/navigation/page-one/content.md new file mode 100644 index 00000000..36d93ec8 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/navigation/page-one/content.md @@ -0,0 +1,6 @@ +--- +type: lesson +title: Page one +--- + +# Navigation test - Page one diff --git a/test/ui/src/content/tutorial/tests/navigation/page-three/content.md b/test/ui/src/content/tutorial/tests/navigation/page-three/content.md new file mode 100644 index 00000000..81e0a02e --- /dev/null +++ b/test/ui/src/content/tutorial/tests/navigation/page-three/content.md @@ -0,0 +1,6 @@ +--- +type: lesson +title: Page three +--- + +# Navigation test - Page three diff --git a/test/ui/src/content/tutorial/tests/navigation/page-two/content.md b/test/ui/src/content/tutorial/tests/navigation/page-two/content.md new file mode 100644 index 00000000..d6d92bc5 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/navigation/page-two/content.md @@ -0,0 +1,6 @@ +--- +type: lesson +title: Page two +--- + +# Navigation test - Page two diff --git a/test/ui/src/content/tutorial/tests/preview/meta.md b/test/ui/src/content/tutorial/tests/preview/meta.md new file mode 100644 index 00000000..c43f388d --- /dev/null +++ b/test/ui/src/content/tutorial/tests/preview/meta.md @@ -0,0 +1,5 @@ +--- +type: chapter +title: Preview +mainCommand: 'pnpm start' +--- diff --git a/test/ui/src/content/tutorial/tests/preview/multiple/content.md b/test/ui/src/content/tutorial/tests/preview/multiple/content.md new file mode 100644 index 00000000..c7509b1d --- /dev/null +++ b/test/ui/src/content/tutorial/tests/preview/multiple/content.md @@ -0,0 +1,9 @@ +--- +type: lesson +title: Multiple +previews: + - [8000, "First Server"] + - [8000, "Second Server", "/about.html"] +--- + +# Preview test - Multiple diff --git a/test/ui/src/content/tutorial/tests/preview/single/content.md b/test/ui/src/content/tutorial/tests/preview/single/content.md new file mode 100644 index 00000000..387761f4 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/preview/single/content.md @@ -0,0 +1,8 @@ +--- +type: lesson +title: Single +previews: + - [8000, "Node Server"] +--- + +# Preview test - Single diff --git a/test/ui/src/env.d.ts b/test/ui/src/env.d.ts new file mode 100644 index 00000000..9505823a --- /dev/null +++ b/test/ui/src/env.d.ts @@ -0,0 +1,3 @@ +/// +/// +/// diff --git a/test/ui/src/templates/default/about.html b/test/ui/src/templates/default/about.html new file mode 100644 index 00000000..b7d6b834 --- /dev/null +++ b/test/ui/src/templates/default/about.html @@ -0,0 +1,14 @@ + + + + + + Default template + + +
+

Default template

+

About

+
+ + diff --git a/test/ui/src/templates/default/index.html b/test/ui/src/templates/default/index.html new file mode 100644 index 00000000..c6c8c3fe --- /dev/null +++ b/test/ui/src/templates/default/index.html @@ -0,0 +1,14 @@ + + + + + + Default template + + +
+

Default template

+

Index

+
+ + diff --git a/test/ui/src/templates/default/index.mjs b/test/ui/src/templates/default/index.mjs new file mode 100644 index 00000000..36969fe0 --- /dev/null +++ b/test/ui/src/templates/default/index.mjs @@ -0,0 +1,23 @@ +import http from 'node:http'; +import { readFileSync } from 'node:fs'; + +const server = http.createServer((req, res) => { + if (req.url === '/' || req.url === '/index.html') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(readFileSync('./index.html', 'utf8')); + + return; + } + + if (req.url === '/about.html') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(readFileSync('./about.html')); + + return; + } + + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('Not found'); +}); + +server.listen(8000); diff --git a/test/ui/src/templates/default/package.json b/test/ui/src/templates/default/package.json new file mode 100644 index 00000000..accec009 --- /dev/null +++ b/test/ui/src/templates/default/package.json @@ -0,0 +1,9 @@ +{ + "name": "default-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "node ./index.mjs" + } +} diff --git a/test/ui/test/file-tree.test.ts b/test/ui/test/file-tree.test.ts new file mode 100644 index 00000000..2f062049 --- /dev/null +++ b/test/ui/test/file-tree.test.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { readLessonFilesAndSolution } from './utils.js'; + +const BASE_URL = '/tests/file-tree'; + +const fixtures = readLessonFilesAndSolution('file-tree/lesson-and-solution', 'file-tree/no-solution'); + +test('user can see lesson and solution files', async ({ page }) => { + const testCase = 'lesson-and-solution'; + await page.goto(`${BASE_URL}/${testCase}`); + + await expect(page.getByRole('heading', { level: 1, name: 'File Tree test - Lesson and solution' })).toBeVisible(); + + // lesson files + for (const file of ['example.html', 'example.js']) { + await page.getByRole('button', { name: file }).click(); + await expect(page.getByRole('button', { name: file, pressed: true })).toBeVisible(); + + await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText(fixtures[testCase].files[file], { + useInnerText: true, + }); + } + + await page.getByRole('button', { name: 'Solve', disabled: false }).click(); + await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); + + // solution files + for (const file of ['example.html', 'example.js']) { + await page.getByRole('button', { name: file }).click(); + await expect(page.getByRole('button', { name: file, pressed: true })).toBeVisible(); + + // TODO: Figure out why this is flaky + await page.waitForTimeout(1_000); + + await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText(fixtures[testCase].solution[file], { + useInnerText: true, + }); + } +}); + +test('user can see cannot click solve on lessons without solution files', async ({ page }) => { + const testCase = 'no-solution'; + await page.goto(`${BASE_URL}/${testCase}`); + + await expect(page.getByRole('heading', { level: 1, name: 'File Tree test - No solution' })).toBeVisible(); + + // lesson files + for (const file of ['example.html', 'example.js']) { + await page.getByRole('button', { name: file }).click(); + await expect(page.getByRole('button', { name: file, pressed: true })).toBeVisible(); + + await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText(fixtures[testCase].files[file], { + useInnerText: true, + }); + } + + // reset-button should be immediately visible + await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); +}); diff --git a/test/ui/test/navigation.test.ts b/test/ui/test/navigation.test.ts new file mode 100644 index 00000000..bf56be55 --- /dev/null +++ b/test/ui/test/navigation.test.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; + +const BASE_URL = '/tests/navigation/page-one'; + +test('user can navigate between lessons using nav bar links', async ({ page }) => { + await page.goto(BASE_URL); + await expect(page.getByRole('heading', { level: 1, name: 'Navigation test - Page one' })).toBeVisible(); + + // navigate forwards + await navigateToPage('Page two'); + await navigateToPage('Page three'); + + // navigate backwards + await navigateToPage('Page two'); + await navigateToPage('Page one'); + + async function navigateToPage(title: string) { + await page.getByRole('link', { name: title }).click(); + await expect(page.getByRole('heading', { level: 1, name: `Navigation test - ${title}` })).toBeVisible(); + } +}); + +test('user can navigate between lessons using breadcrumbs', async ({ page }) => { + await page.goto(BASE_URL); + + await page.getByRole('button', { name: 'Tests / Navigation / Page one' }).click({ force: true }); + await page.getByRole('region', { name: 'Navigation' }).getByRole('link', { name: 'Page three' }).click(); + + await expect(page.getByRole('heading', { level: 1, name: 'Navigation test - Page three' })).toBeVisible(); +}); diff --git a/test/ui/test/preview.test.ts b/test/ui/test/preview.test.ts new file mode 100644 index 00000000..ec772d32 --- /dev/null +++ b/test/ui/test/preview.test.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test'; + +const BASE_URL = '/tests/preview'; + +test('user can see single preview tab', async ({ page }) => { + await page.goto(`${BASE_URL}/single`); + + await expect(page.getByRole('heading', { level: 1, name: 'Preview test - Single' })).toBeVisible(); + + await expect(page.getByText('Node Server')).toBeVisible(); + + const preview = page.frameLocator('[title="Node Server"]'); + await expect(preview.getByText('Default template')).toBeVisible(); + await expect(preview.getByText('Index')).toBeVisible(); +}); + +test('user can see multiple preview tabs', async ({ page }) => { + await page.goto(`${BASE_URL}/multiple`); + + await expect(page.getByRole('heading', { level: 1, name: 'Preview test - Multiple' })).toBeVisible(); + + await expect(page.getByText('First Server')).toBeVisible(); + await expect(page.getByText('Second Server')).toBeVisible(); + + await expect(page.frameLocator('[title="First Server"]').getByText('Index')).toBeVisible({ timeout: 10_000 }); + await expect(page.frameLocator('[title="Second Server"]').getByText('About')).toBeVisible({ timeout: 10_000 }); +}); diff --git a/test/ui/test/utils.ts b/test/ui/test/utils.ts new file mode 100644 index 00000000..d874e093 --- /dev/null +++ b/test/ui/test/utils.ts @@ -0,0 +1,33 @@ +import { readdirSync, readFileSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const TESTS_DIR = fileURLToPath(new URL('../src/content/tutorial/tests', import.meta.url)); + +export function readLessonFilesAndSolution( + ...lessons: string[] +): Record; solution: Record }> { + return lessons.reduce( + (all, lesson) => ({ + ...all, + [lesson.split('/')[1]]: { + files: readDirFiles(`${TESTS_DIR}/${lesson}/_files`), + solution: readDirFiles(`${TESTS_DIR}/${lesson}/_solution`), + }, + }), + {}, + ); +} + +function readDirFiles(dir: string): Record { + if (!existsSync(dir)) { + return {}; + } + + return readdirSync(dir).reduce( + (files, file) => ({ + ...files, + [file]: readFileSync(`${dir}/${file}`, 'utf8'), + }), + {}, + ); +} diff --git a/test/ui/uno.config.ts b/test/ui/uno.config.ts new file mode 100644 index 00000000..7018afe8 --- /dev/null +++ b/test/ui/uno.config.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs/promises'; +import { basename, dirname, join } from 'node:path'; +import { globSync, convertPathToPattern } from 'fast-glob'; +import { defineConfig, presetIcons, presetUno, transformerDirectives } from 'unocss'; +import { rules, shortcuts, theme } from '@tutorialkit/theme'; + +const iconPaths = globSync('./icons/languages/*.svg'); + +const customIconCollection = iconPaths.reduce( + (acc, iconPath) => { + const collectionName = basename(dirname(iconPath)); + const [iconName] = basename(iconPath).split('.'); + + acc[collectionName] ??= {}; + acc[collectionName][iconName] = async () => fs.readFile(iconPath, 'utf8'); + + return acc; + }, + {} as Record Promise>>, +); + +export default defineConfig({ + rules, + shortcuts, + theme, + content: { + inline: globSync([ + `${convertPathToPattern(join(require.resolve('@tutorialkit/components-react'), '..')).replace('\\@', '/@')}/**/*.js`, + `${convertPathToPattern(join(require.resolve('@tutorialkit/astro'), '..')).replace('\\@', '/@')}/default/**/*.astro`, + ]).map((filePath) => { + return () => fs.readFile(filePath, { encoding: 'utf8' }); + }), + }, + transformers: [transformerDirectives()], + presets: [ + presetUno({ + dark: { + dark: '[data-theme="dark"]', + }, + }), + presetIcons({ + collections: { + ...customIconCollection, + }, + }), + ], +});