Skip to content

Commit af081ad

Browse files
committed
Install Playwright and Convert First Siesta Test #7254
1 parent 270f714 commit af081ad

7 files changed

Lines changed: 269 additions & 1 deletion

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Ticket: Install Playwright and Convert First Siesta Test
2+
3+
GH ticket id: #7254
4+
5+
**Assignee:** Gemini
6+
**Status:** Done
7+
8+
## Description
9+
Install the Playwright test runner and convert an initial Siesta unit test to the new framework to establish a baseline pattern for future test migration.
10+
11+
### Implementation Notes
12+
The initial approach was to convert the VDOM unit test into a browser-based component integration test. However, the goal was refined to keep it as a pure VDOM unit test running within a Node.js environment.
13+
14+
The final, successful implementation uses Playwright's test runner without a browser. It works by:
15+
1. Creating a `setup.mjs` file to mock the necessary browser globals (`DOMRect`) and configure the `Neo` namespace on `globalThis` for a headless environment.
16+
2. Configuring `playwright.config.mjs` to run without a browser or web server.
17+
3. Writing the test spec as a pure Node.js module, importing the framework classes directly and asserting against the VDOM data structures returned by the component's methods.
18+
19+
This pattern allows for fast, browser-less unit testing of framework components using the Playwright test runner.
20+
21+
## Tasks
22+
- [x] Install `@playwright/test` dependency.
23+
- [x] Create `playwright.config.mjs` for a Node.js test environment.
24+
- [x] Create `test/playwright/setup.mjs` to configure the Node.js global scope.
25+
- [x] Convert `test/siesta/tests/classic/Button.mjs` to `test/playwright/classic/button.spec.mjs`.
26+
- [x] All tests are passing.
27+
- [x] Move `playwright.config.mjs` into `test/playwright/` and update paths.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,6 @@ resources/data/deck/EditorConfig.json
8383
# Local AI Knowledge Base
8484
.env
8585
chroma/
86+
87+
# Playwright test results
88+
test/playwright/test-results.json

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"generate-docs-json" : "node ./buildScripts/docs/jsdocx.mjs",
3434
"inject-package-version": "node ./buildScripts/injectPackageVersion.mjs",
3535
"server-start" : "webpack serve -c ./buildScripts/webpack/webpack.server.config.mjs --open",
36-
"test" : "echo \"Error: no test specified\" && exit 1",
36+
"test" : "playwright test -c test/playwright/playwright.config.mjs",
3737
"watch-themes" : "node ./buildScripts/watchThemes.mjs"
3838
},
3939
"keywords": [
@@ -87,6 +87,7 @@
8787
"@chroma-core/default-embed" : "^0.1.8",
8888
"@fortawesome/fontawesome-free": "^7.0.1",
8989
"@google/generative-ai" : "^0.24.1",
90+
"@playwright/test" : "^1.55.1",
9091
"acorn" : "^8.15.0",
9192
"astring" : "^1.9.0",
9293
"autoprefixer" : "^10.4.21",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import '../setup.mjs'; // Sets up the globalThis.Neo object
2+
3+
import { test, expect } from '@playwright/test';
4+
5+
// Correct import order is critical for Neo.mjs initialization
6+
import Neo from '../../../src/Neo.mjs';
7+
import * as core from '../../../src/core/_export.mjs';
8+
import Button from '../../../src/button/Base.mjs';
9+
import DomApiVnodeCreator from '../../../src/vdom/util/DomApiVnodeCreator.mjs';
10+
import VdomHelper from '../../../src/vdom/Helper.mjs';
11+
12+
13+
const appName = 'ClassicButtonTest';
14+
15+
test.describe('Neo.button.Base VDOM (Node.js)', () => {
16+
17+
test('should create initial vnode correctly', async () => {
18+
const button = Neo.create(Button, {
19+
appName,
20+
iconCls: 'fa fa-home',
21+
text : 'Click me'
22+
});
23+
const { vnode } = await button.initVnode();
24+
button.destroy();
25+
26+
expect(vnode.nodeName).toBe('button');
27+
expect(vnode.className).toEqual(['neo-button', 'icon-left']);
28+
expect(vnode.childNodes.length).toBe(2);
29+
30+
const iconNode = vnode.childNodes[0];
31+
expect(iconNode.className).toEqual(['neo-button-glyph', 'fa', 'fa-home']);
32+
33+
const textNode = vnode.childNodes[1];
34+
expect(textNode.className).toEqual(['neo-button-text']);
35+
expect(textNode.textContent).toBe('Click me');
36+
});
37+
38+
test('should update vnode and create delta for a single config change', async () => {
39+
const button = Neo.create(Button, {
40+
appName,
41+
text: 'Click me'
42+
});
43+
await button.initVnode();
44+
button.mounted = true;
45+
46+
const textNodeId = button.vnode.childNodes[0].id;
47+
const { deltas } = await button.set({text: 'New Text'});
48+
button.destroy();
49+
50+
expect(deltas.length).toBe(1);
51+
const delta = deltas[0];
52+
expect(delta.id).toBe(textNodeId);
53+
expect(delta.textContent).toBe('New Text');
54+
});
55+
56+
test('should update vnode and create delta for multiple config changes', async () => {
57+
const button = Neo.create(Button, {
58+
appName,
59+
iconCls: 'fa fa-home',
60+
text : 'Click me'
61+
});
62+
await button.initVnode();
63+
button.mounted = true;
64+
65+
const iconNodeId = button.vnode.childNodes[0].id;
66+
const textNodeId = button.vnode.childNodes[1].id;
67+
68+
const { deltas } = await button.set({
69+
iconCls: 'fa fa-user',
70+
text : 'Submit'
71+
});
72+
button.destroy();
73+
74+
expect(deltas.length).toBe(2);
75+
76+
const iconDelta = deltas.find(d => d.id === iconNodeId);
77+
const textDelta = deltas.find(d => d.id === textNodeId);
78+
79+
expect(iconDelta).toBeDefined();
80+
expect(iconDelta.cls.remove).toEqual(['fa-home']);
81+
expect(iconDelta.cls.add).toEqual(['fa-user']);
82+
83+
expect(textDelta).toBeDefined();
84+
expect(textDelta.textContent).toBe('Submit');
85+
});
86+
87+
test('should handle pressed state change', async () => {
88+
const button = Neo.create(Button, {
89+
appName
90+
});
91+
const {vnode} = await button.initVnode();
92+
button.mounted = true;
93+
94+
expect(vnode.className.includes('pressed')).toBe(false);
95+
96+
let updateData = await button.set({pressed: true});
97+
expect(updateData.deltas.length).toBe(1);
98+
let delta = updateData.deltas[0];
99+
expect(delta.id).toBe(button.id);
100+
expect(delta.cls.add).toEqual(['pressed']);
101+
expect(updateData.vnode.className.includes('pressed')).toBe(true);
102+
103+
updateData = await button.set({pressed: false});
104+
expect(updateData.deltas.length).toBe(1);
105+
delta = updateData.deltas[0];
106+
expect(delta.id).toBe(button.id);
107+
expect(delta.cls.remove).toEqual(['pressed']);
108+
expect(updateData.vnode.className.includes('pressed')).toBe(false);
109+
110+
button.destroy();
111+
});
112+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
/**
4+
* @see https://playwright.dev/docs/test-configuration
5+
*/
6+
export default defineConfig({
7+
testDir: '.',
8+
/* Run tests in files in parallel */
9+
fullyParallel: true,
10+
/* Fail the build on CI if you accidentally left test.only in the source code. */
11+
forbidOnly: !!process.env.CI,
12+
/* Retry on CI only */
13+
retries: process.env.CI ? 2 : 0,
14+
/* Opt out of parallel tests on CI. */
15+
workers: process.env.CI ? 1 : undefined,
16+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
17+
reporter: [['json', { outputFile: 'test-results.json' }]],
18+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
19+
use: {
20+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
21+
trace: 'on-first-retry',
22+
},
23+
});

test/playwright/setup.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// This file sets up the Node.js global scope to run Neo.mjs
2+
// VDOM unit tests without a browser or jsdom.
3+
4+
const appName = 'ClassicButtonTest';
5+
6+
globalThis.Neo = {
7+
apps: {
8+
[appName]: {
9+
name : appName,
10+
fire : () => {},
11+
isMounted : () => true,
12+
vnodeInitialising: false
13+
}
14+
},
15+
config: {
16+
allowVdomUpdatesInTests: true,
17+
environment : 'development',
18+
unitTestMode : true,
19+
useDomApiRenderer : true
20+
}
21+
};
22+
23+
// Running the unit tests directly inside nodejs requires a mock for DOMRect.
24+
globalThis.DOMRect = class DOMRect {
25+
constructor(x, y, width, height) {
26+
this.x = x || 0;
27+
this.y = y || 0;
28+
this.width = width || 0;
29+
this.height = height || 0;
30+
this.top = this.y;
31+
this.left = this.x;
32+
this.right = this.x + this.width;
33+
this.bottom = this.y + this.height;
34+
}
35+
static fromRect(other) {
36+
return new DOMRect(other.x, other.y, other.width, other.height);
37+
}
38+
};

0 commit comments

Comments
 (0)