Skip to content

Commit 74e0527

Browse files
authored
test: add tests for ui (#241)
1 parent a580bb5 commit 74e0527

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+846
-16
lines changed

.github/workflows/ci.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ jobs:
3535
- name: Test
3636
run: pnpm test
3737

38+
test-e2e:
39+
name: Test E2E
40+
runs-on: ubuntu-latest
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@v4
44+
45+
- uses: ./.github/actions/setup-and-build
46+
47+
- name: Install Playwright Dependencies
48+
run: pnpm --filter=tutorialkit-e2e exec playwright install chromium --with-deps
49+
50+
- name: Test
51+
run: pnpm test:e2e
52+
3853
docs:
3954
name: Docs
4055
runs-on: ubuntu-latest

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ tsconfig.tsbuildinfo
2323
tsconfig.build.tsbuildinfo
2424
.tmp
2525
.tmp-*
26+
/e2e/**/test-results
27+
/e2e/**/.astro

.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
shell-emulator=true

e2e/README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# UI Tests
2+
3+
> Tests for verifying TutorialKit works as expected in the browser. Tests are run against locally linked `@tutorialkit` packages.
4+
5+
## Running
6+
7+
- `pnpm exec playwright install chromium --with-deps` - When running the tests first time
8+
- `pnpm test`
9+
10+
## Development
11+
12+
- `pnpm start` - Starts example/fixture project's development server
13+
- `pnpm test:ui` - Start Playwright in UI mode
14+
15+
## Structure
16+
17+
Test cases are located in `test` directory.
18+
Each test file has its own `chapter`, that contains `lesson`s for test cases:
19+
20+
For example Navigation tests:
21+
22+
```
23+
├── src/content/tutorial
24+
│ └── tests
25+
│ └──── navigation
26+
│ ├── page-one
27+
│ ├── page-three
28+
│ └── page-two
29+
└── test
30+
└── navigation.test.ts
31+
```
32+
33+
Or File Tree tests:
34+
35+
```
36+
├── src/content/tutorial
37+
│ └── tests
38+
│ └── file-tree
39+
│ └── lesson-and-solution
40+
└── test
41+
└── file-tree.test.ts
42+
```

e2e/astro.config.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createRequire } from 'node:module';
2+
import { resolve } from 'node:path';
3+
import tutorialkit from '@tutorialkit/astro';
4+
import { defineConfig } from 'astro/config';
5+
6+
const require = createRequire(import.meta.url);
7+
const astroDist = resolve(require.resolve('astro/package.json'), '..');
8+
const swapFunctionEntry = resolve(astroDist, 'dist/transitions/swap-functions.js');
9+
10+
export default defineConfig({
11+
devToolbar: { enabled: false },
12+
server: { port: 4329 },
13+
integrations: [tutorialkit()],
14+
15+
vite: {
16+
resolve: {
17+
alias: {
18+
// work-around for https://github.com/stackblitz/tutorialkit/pull/238
19+
'node_modules/astro/dist/transitions/swap-functions': swapFunctionEntry,
20+
},
21+
},
22+
},
23+
});

e2e/package.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "tutorialkit-e2e",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "astro dev",
7+
"preview": "astro build && astro preview",
8+
"test": "playwright test",
9+
"test:ui": "pnpm run test --ui"
10+
},
11+
"devDependencies": {
12+
"@astrojs/react": "^3.6.0",
13+
"@iconify-json/ph": "^1.1.13",
14+
"@iconify-json/svg-spinners": "^1.1.2",
15+
"@playwright/test": "^1.46.0",
16+
"@tutorialkit/astro": "workspace:*",
17+
"@tutorialkit/components-react": "workspace:*",
18+
"@tutorialkit/runtime": "workspace:*",
19+
"@tutorialkit/theme": "workspace:*",
20+
"@tutorialkit/types": "workspace:*",
21+
"@types/node": "^22.2.0",
22+
"@unocss/reset": "^0.59.4",
23+
"@unocss/transformer-directives": "^0.62.0",
24+
"astro": "^4.12.0",
25+
"fast-glob": "^3.3.2",
26+
"playwright": "^1.46.0",
27+
"react": "^18.3.1",
28+
"react-dom": "^18.3.1",
29+
"unocss": "^0.59.4"
30+
}
31+
}

e2e/playwright.config.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
export default defineConfig({
4+
expect: {
5+
timeout: process.env.CI ? 30_000 : 10_000,
6+
},
7+
use: {
8+
baseURL: 'http://localhost:4329',
9+
},
10+
webServer: {
11+
command: 'pnpm preview',
12+
url: 'http://localhost:4329',
13+
reuseExistingServer: !process.env.CI,
14+
stdout: 'ignore',
15+
stderr: 'pipe',
16+
},
17+
});

e2e/public/logo.svg

+1
Loading

e2e/src/content/config.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { contentSchema } from '@tutorialkit/types';
2+
import { defineCollection } from 'astro:content';
3+
4+
const tutorial = defineCollection({
5+
type: 'content',
6+
schema: contentSchema,
7+
});
8+
9+
export const collections = { tutorial };

e2e/src/content/tutorial/meta.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: tutorial
3+
mainCommand: ''
4+
prepareCommands: []
5+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html>
2+
<head>
3+
<title>Lesson file example.html title</title>
4+
</head>
5+
<body>
6+
Lesson file example.html content
7+
</body>
8+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'Lesson file example.js content';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html>
2+
<head>
3+
<title>Solution file example.html title</title>
4+
</head>
5+
<body>
6+
Solution file example.html content
7+
</body>
8+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'Solution file example.js content';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Lesson and solution
4+
---
5+
6+
# File Tree test - Lesson and solution
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: chapter
3+
title: File Tree
4+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html>
2+
<head>
3+
<title>Lesson file example.html title</title>
4+
</head>
5+
<body>
6+
Lesson file example.html content
7+
</body>
8+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'Lesson file example.js content';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: No solution
4+
---
5+
6+
# File Tree test - No solution
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
type: part
3+
title: Tests
4+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
type: chapter
3+
title: Navigation
4+
lessons:
5+
- page-one
6+
- page-two
7+
- page-three
8+
mainCommand: ''
9+
prepareCommands: []
10+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Page one
4+
---
5+
6+
# Navigation test - Page one
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Page three
4+
---
5+
6+
# Navigation test - Page three
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Page two
4+
---
5+
6+
# Navigation test - Page two
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: chapter
3+
title: Preview
4+
mainCommand: 'node ./index.mjs'
5+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
type: lesson
3+
title: Multiple
4+
previews:
5+
- [8000, "First Server"]
6+
- [8000, "Second Server", "/about.html"]
7+
---
8+
9+
# Preview test - Multiple
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: lesson
3+
title: Single
4+
previews:
5+
- [8000, "Node Server"]
6+
---
7+
8+
# Preview test - Single

e2e/src/env.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// <reference path="../.astro/types.d.ts" />
2+
/// <reference types="@tutorialkit/astro/types" />
3+
/// <reference types="astro/client" />

e2e/src/templates/default/index.mjs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import http from 'node:http';
2+
3+
const server = http.createServer((req, res) => {
4+
if (req.url === '/' || req.url === '/index.html') {
5+
res.writeHead(200, { 'Content-Type': 'text/html' });
6+
res.end('Index page');
7+
8+
return;
9+
}
10+
11+
if (req.url === '/about.html') {
12+
res.writeHead(200, { 'Content-Type': 'text/html' });
13+
res.end('About page');
14+
15+
return;
16+
}
17+
18+
res.writeHead(200, { 'Content-Type': 'text/html' });
19+
res.end('Not found');
20+
});
21+
22+
server.listen(8000);

e2e/test/file-tree.test.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { test, expect } from '@playwright/test';
2+
import { readLessonFilesAndSolution } from './utils.js';
3+
4+
const BASE_URL = '/tests/file-tree';
5+
6+
const fixtures = readLessonFilesAndSolution('file-tree/lesson-and-solution', 'file-tree/no-solution');
7+
8+
test('user can see lesson and solution files', async ({ page }) => {
9+
const testCase = 'lesson-and-solution';
10+
await page.goto(`${BASE_URL}/${testCase}`);
11+
12+
await expect(page.getByRole('heading', { level: 1, name: 'File Tree test - Lesson and solution' })).toBeVisible();
13+
14+
// lesson files
15+
for (const file of ['example.html', 'example.js']) {
16+
await page.getByRole('button', { name: file }).click();
17+
await expect(page.getByRole('button', { name: file, pressed: true })).toBeVisible();
18+
19+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText(fixtures[testCase].files[file], {
20+
useInnerText: true,
21+
});
22+
}
23+
24+
await page.getByRole('button', { name: 'Solve', disabled: false }).click();
25+
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
26+
27+
// solution files
28+
for (const file of ['example.html', 'example.js']) {
29+
await page.getByRole('button', { name: file }).click();
30+
await expect(page.getByRole('button', { name: file, pressed: true })).toBeVisible();
31+
32+
// TODO: Figure out why this is flaky
33+
await page.waitForTimeout(1_000);
34+
35+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText(fixtures[testCase].solution[file], {
36+
useInnerText: true,
37+
});
38+
}
39+
});
40+
41+
test('user can see cannot click solve on lessons without solution files', async ({ page }) => {
42+
const testCase = 'no-solution';
43+
await page.goto(`${BASE_URL}/${testCase}`);
44+
45+
await expect(page.getByRole('heading', { level: 1, name: 'File Tree test - No solution' })).toBeVisible();
46+
47+
// lesson files
48+
for (const file of ['example.html', 'example.js']) {
49+
await page.getByRole('button', { name: file }).click();
50+
await expect(page.getByRole('button', { name: file, pressed: true })).toBeVisible();
51+
52+
await expect(page.getByRole('textbox', { name: 'Editor' })).toHaveText(fixtures[testCase].files[file], {
53+
useInnerText: true,
54+
});
55+
}
56+
57+
// reset-button should be immediately visible
58+
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
59+
});

e2e/test/navigation.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const BASE_URL = '/tests/navigation/page-one';
4+
5+
test('user can navigate between lessons using nav bar links', async ({ page }) => {
6+
await page.goto(BASE_URL);
7+
await expect(page.getByRole('heading', { level: 1, name: 'Navigation test - Page one' })).toBeVisible();
8+
9+
// navigate forwards
10+
await navigateToPage('Page two');
11+
await navigateToPage('Page three');
12+
13+
// navigate backwards
14+
await navigateToPage('Page two');
15+
await navigateToPage('Page one');
16+
17+
async function navigateToPage(title: string) {
18+
await page.getByRole('link', { name: title }).click();
19+
await expect(page.getByRole('heading', { level: 1, name: `Navigation test - ${title}` })).toBeVisible();
20+
}
21+
});
22+
23+
test('user can navigate between lessons using breadcrumbs', async ({ page }) => {
24+
await page.goto(BASE_URL);
25+
26+
await page.getByRole('button', { name: 'Tests / Navigation / Page one' }).click({ force: true });
27+
await page.getByRole('region', { name: 'Navigation' }).getByRole('link', { name: 'Page three' }).click();
28+
29+
await expect(page.getByRole('heading', { level: 1, name: 'Navigation test - Page three' })).toBeVisible();
30+
});

0 commit comments

Comments
 (0)