@omicronenergy/oscd-shell 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +22 -0
- package/.github/actions/setup-node-env/action.yml +49 -0
- package/.github/workflows/release-please.yml +84 -0
- package/.github/workflows/test.yml +18 -0
- package/.github/workflows/update-screenshots.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.release-please-manifest.json +1 -0
- package/CHANGELOG.md +73 -0
- package/CONTRIBUTING.md +393 -0
- package/LICENSE +201 -0
- package/README.md +74 -0
- package/commitlint.config.js +1 -0
- package/demo/AddPlugins.js +42 -0
- package/demo/DemoPluginSrc.js +9 -0
- package/demo/embedded.html +17 -0
- package/demo/index.html +95 -0
- package/foundation/cyrb64.ts +27 -0
- package/foundation/edit-event.ts +273 -0
- package/foundation/open-event.ts +21 -0
- package/foundation.ts +28 -0
- package/lit-localize.json +15 -0
- package/localization/de.xlf +75 -0
- package/oscd-shell.editing.spec.ts +813 -0
- package/oscd-shell.plugging.spec.ts +78 -0
- package/oscd-shell.spec.ts +431 -0
- package/oscd-shell.test.ts +382 -0
- package/oscd-shell.ts +906 -0
- package/package.json +195 -0
- package/release-please-config.json +21 -0
- package/rollup.config.js +51 -0
- package/screenshots/Chromium/baseline/app-bar-de.png +0 -0
- package/screenshots/Chromium/baseline/app-bar-en.png +0 -0
- package/screenshots/Chromium/baseline/document-name-de.png +0 -0
- package/screenshots/Chromium/baseline/document-name-en.png +0 -0
- package/screenshots/Chromium/baseline/editor-plugins-de.png +0 -0
- package/screenshots/Chromium/baseline/editor-plugins-en.png +0 -0
- package/screenshots/Chromium/baseline/editor-plugins-selected-de.png +0 -0
- package/screenshots/Chromium/baseline/editor-plugins-selected-en.png +0 -0
- package/screenshots/Chromium/baseline/editor-plugins-with-doc-de.png +0 -0
- package/screenshots/Chromium/baseline/editor-plugins-with-doc-en.png +0 -0
- package/screenshots/Chromium/baseline/log-entries-de.png +0 -0
- package/screenshots/Chromium/baseline/log-entries-en.png +0 -0
- package/screenshots/Chromium/baseline/log-entries-redone-de.png +0 -0
- package/screenshots/Chromium/baseline/log-entries-redone-en.png +0 -0
- package/screenshots/Chromium/baseline/log-entries-undone-de.png +0 -0
- package/screenshots/Chromium/baseline/log-entries-undone-en.png +0 -0
- package/screenshots/Chromium/baseline/log-screen-de.png +0 -0
- package/screenshots/Chromium/baseline/log-screen-en.png +0 -0
- package/screenshots/Chromium/baseline/menu-drawer-de.png +0 -0
- package/screenshots/Chromium/baseline/menu-drawer-en.png +0 -0
- package/screenshots/Chromium/baseline/menu-plugins-de.png +0 -0
- package/screenshots/Chromium/baseline/menu-plugins-en.png +0 -0
- package/screenshots/Chromium/baseline/menu-plugins-triggered-de.png +0 -0
- package/screenshots/Chromium/baseline/menu-plugins-triggered-en.png +0 -0
- package/screenshots/Firefox/baseline/app-bar-de.png +0 -0
- package/screenshots/Firefox/baseline/app-bar-en.png +0 -0
- package/screenshots/Firefox/baseline/document-name-de.png +0 -0
- package/screenshots/Firefox/baseline/document-name-en.png +0 -0
- package/screenshots/Firefox/baseline/editor-plugins-de.png +0 -0
- package/screenshots/Firefox/baseline/editor-plugins-en.png +0 -0
- package/screenshots/Firefox/baseline/editor-plugins-selected-de.png +0 -0
- package/screenshots/Firefox/baseline/editor-plugins-selected-en.png +0 -0
- package/screenshots/Firefox/baseline/editor-plugins-with-doc-de.png +0 -0
- package/screenshots/Firefox/baseline/editor-plugins-with-doc-en.png +0 -0
- package/screenshots/Firefox/baseline/log-entries-de.png +0 -0
- package/screenshots/Firefox/baseline/log-entries-en.png +0 -0
- package/screenshots/Firefox/baseline/log-entries-redone-de.png +0 -0
- package/screenshots/Firefox/baseline/log-entries-redone-en.png +0 -0
- package/screenshots/Firefox/baseline/log-entries-undone-de.png +0 -0
- package/screenshots/Firefox/baseline/log-entries-undone-en.png +0 -0
- package/screenshots/Firefox/baseline/log-screen-de.png +0 -0
- package/screenshots/Firefox/baseline/log-screen-en.png +0 -0
- package/screenshots/Firefox/baseline/menu-drawer-de.png +0 -0
- package/screenshots/Firefox/baseline/menu-drawer-en.png +0 -0
- package/screenshots/Firefox/baseline/menu-plugins-de.png +0 -0
- package/screenshots/Firefox/baseline/menu-plugins-en.png +0 -0
- package/screenshots/Firefox/baseline/menu-plugins-triggered-de.png +0 -0
- package/screenshots/Firefox/baseline/menu-plugins-triggered-en.png +0 -0
- package/screenshots/Webkit/baseline/app-bar-de.png +0 -0
- package/screenshots/Webkit/baseline/app-bar-en.png +0 -0
- package/screenshots/Webkit/baseline/document-name-de.png +0 -0
- package/screenshots/Webkit/baseline/document-name-en.png +0 -0
- package/screenshots/Webkit/baseline/editor-plugins-de.png +0 -0
- package/screenshots/Webkit/baseline/editor-plugins-en.png +0 -0
- package/screenshots/Webkit/baseline/editor-plugins-selected-de.png +0 -0
- package/screenshots/Webkit/baseline/editor-plugins-selected-en.png +0 -0
- package/screenshots/Webkit/baseline/editor-plugins-with-doc-de.png +0 -0
- package/screenshots/Webkit/baseline/editor-plugins-with-doc-en.png +0 -0
- package/screenshots/Webkit/baseline/log-entries-de.png +0 -0
- package/screenshots/Webkit/baseline/log-entries-en.png +0 -0
- package/screenshots/Webkit/baseline/log-entries-redone-de.png +0 -0
- package/screenshots/Webkit/baseline/log-entries-redone-en.png +0 -0
- package/screenshots/Webkit/baseline/log-entries-undone-de.png +0 -0
- package/screenshots/Webkit/baseline/log-entries-undone-en.png +0 -0
- package/screenshots/Webkit/baseline/log-screen-de.png +0 -0
- package/screenshots/Webkit/baseline/log-screen-en.png +0 -0
- package/screenshots/Webkit/baseline/menu-drawer-de.png +0 -0
- package/screenshots/Webkit/baseline/menu-drawer-en.png +0 -0
- package/screenshots/Webkit/baseline/menu-plugins-de.png +0 -0
- package/screenshots/Webkit/baseline/menu-plugins-en.png +0 -0
- package/screenshots/Webkit/baseline/menu-plugins-triggered-de.png +0 -0
- package/screenshots/Webkit/baseline/menu-plugins-triggered-en.png +0 -0
- package/tsconfig.json +23 -0
- package/utils/testing.spec.ts +210 -0
- package/utils/testing.ts +65 -0
- package/web-dev-server.config.js +24 -0
- package/web-test-runner.config.js +180 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { expect, fixture, html } from '@open-wc/testing';
|
|
2
|
+
import sinon from 'sinon';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
findButtonByIcon,
|
|
6
|
+
getFirstTextNodeContent,
|
|
7
|
+
querySelectorContainingText,
|
|
8
|
+
simulateKeypressOnElement,
|
|
9
|
+
waitForDialogState,
|
|
10
|
+
} from './testing.js';
|
|
11
|
+
|
|
12
|
+
describe('querySelectorContainingText', () => {
|
|
13
|
+
it('returns the element matching selector and exact text', async () => {
|
|
14
|
+
const el = await fixture<HTMLDivElement>(html`
|
|
15
|
+
<div>
|
|
16
|
+
<span class="item">Apple</span>
|
|
17
|
+
<span class="item">Banana</span>
|
|
18
|
+
<span class="item">Cherry</span>
|
|
19
|
+
</div>
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
const result = querySelectorContainingText(el, '.item', 'Banana');
|
|
23
|
+
expect(result).to.exist;
|
|
24
|
+
expect(result?.textContent?.trim()).to.equal('Banana');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns undefined if no element matches the text', async () => {
|
|
28
|
+
const el = await fixture<HTMLDivElement>(html`
|
|
29
|
+
<div>
|
|
30
|
+
<span class="item">Apple</span>
|
|
31
|
+
<span class="item">Banana</span>
|
|
32
|
+
</div>
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
const result = querySelectorContainingText(el, '.item', 'Orange');
|
|
36
|
+
expect(result).to.be.undefined;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns undefined if no element matches the selector', async () => {
|
|
40
|
+
const el = await fixture<HTMLDivElement>(html`
|
|
41
|
+
<div>
|
|
42
|
+
<span class="item">Apple</span>
|
|
43
|
+
</div>
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
const result = querySelectorContainingText(el, '.notfound', 'Apple');
|
|
47
|
+
expect(result).to.be.undefined;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('matches only the first text node content', async () => {
|
|
51
|
+
const el = await fixture<HTMLDivElement>(html`
|
|
52
|
+
<div>
|
|
53
|
+
<span class="item">Apple <b>Bold</b></span>
|
|
54
|
+
<span class="item">Banana</span>
|
|
55
|
+
</div>
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
const result = querySelectorContainingText(el, '.item', 'Apple');
|
|
59
|
+
expect(result).to.exist;
|
|
60
|
+
expect(result?.textContent?.includes('Apple')).to.be.true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('ignores whitespace differences in text nodes', async () => {
|
|
64
|
+
const el = await fixture<HTMLDivElement>(html`
|
|
65
|
+
<div>
|
|
66
|
+
<span class="item"> Apple </span>
|
|
67
|
+
</div>
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
const result = querySelectorContainingText(el, '.item', 'Apple');
|
|
71
|
+
expect(result).to.exist;
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('getFirstTextNodeContent', () => {
|
|
75
|
+
it('returns the trimmed text content of the first text node', async () => {
|
|
76
|
+
const el = document.createElement('div');
|
|
77
|
+
el.appendChild(document.createTextNode(' Hello '));
|
|
78
|
+
el.appendChild(document.createElement('span'));
|
|
79
|
+
expect(getFirstTextNodeContent(el)).to.equal('Hello');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns undefined if there are no text nodes', () => {
|
|
83
|
+
const el = document.createElement('div');
|
|
84
|
+
el.appendChild(document.createElement('span'));
|
|
85
|
+
expect(getFirstTextNodeContent(el)).to.be.undefined;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns empty string if element is null', () => {
|
|
89
|
+
expect(getFirstTextNodeContent(null)).to.equal('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('ignores empty text nodes', () => {
|
|
93
|
+
const el = document.createElement('div');
|
|
94
|
+
el.appendChild(document.createTextNode(' '));
|
|
95
|
+
el.appendChild(document.createTextNode('Text'));
|
|
96
|
+
expect(getFirstTextNodeContent(el)).to.equal('Text');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns an empty string if no element is passed in', () => {
|
|
100
|
+
expect(getFirstTextNodeContent(null)).to.equal('');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('simulateKeypressOnElement', () => {
|
|
105
|
+
it('dispatches a keydown event with the correct key and ctrlKey', () => {
|
|
106
|
+
const spy = sinon.spy();
|
|
107
|
+
document.addEventListener('keydown', spy, { once: true });
|
|
108
|
+
simulateKeypressOnElement('a', true);
|
|
109
|
+
expect(spy.calledOnce).to.be.true;
|
|
110
|
+
const event = spy.firstCall.args[0];
|
|
111
|
+
expect(event).to.have.property('key', 'a');
|
|
112
|
+
expect(event).to.have.property('ctrlKey', true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('waitForDialogState', () => {
|
|
117
|
+
it('resolves immediately if dialog is already open', async () => {
|
|
118
|
+
const dialog = document.createElement('dialog');
|
|
119
|
+
dialog.setAttribute('open', '');
|
|
120
|
+
let resolved = false;
|
|
121
|
+
await waitForDialogState(dialog, 'open').then(() => {
|
|
122
|
+
resolved = true;
|
|
123
|
+
});
|
|
124
|
+
expect(resolved).to.be.true;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('waits for dialog to become open', async () => {
|
|
128
|
+
const dialog = document.createElement('dialog');
|
|
129
|
+
setTimeout(() => dialog.setAttribute('open', ''), 10);
|
|
130
|
+
await waitForDialogState(dialog, 'open');
|
|
131
|
+
expect(dialog.hasAttribute('open')).to.be.true;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('resolves immediately if dialog is already closed', async () => {
|
|
135
|
+
const dialog = document.createElement('dialog');
|
|
136
|
+
let resolved = false;
|
|
137
|
+
await waitForDialogState(dialog, 'closed').then(() => {
|
|
138
|
+
resolved = true;
|
|
139
|
+
});
|
|
140
|
+
expect(resolved).to.be.true;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('waits for dialog to emit closed event', async () => {
|
|
144
|
+
const dialog = document.createElement('dialog');
|
|
145
|
+
dialog.setAttribute('open', '');
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
dialog.removeAttribute('open');
|
|
148
|
+
dialog.dispatchEvent(new Event('closed'));
|
|
149
|
+
}, 10);
|
|
150
|
+
await waitForDialogState(dialog, 'closed');
|
|
151
|
+
expect(dialog.hasAttribute('open')).to.be.false;
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('findButtonByIcon', () => {
|
|
156
|
+
it('returns the button with the matching icon name', () => {
|
|
157
|
+
const root = document.createElement('div');
|
|
158
|
+
const btn1 = document.createElement('button');
|
|
159
|
+
btn1.classList.add('icon-btn');
|
|
160
|
+
const icon1 = document.createElement('oscd-icon');
|
|
161
|
+
icon1.textContent = 'foo';
|
|
162
|
+
btn1.appendChild(icon1);
|
|
163
|
+
|
|
164
|
+
const btn2 = document.createElement('button');
|
|
165
|
+
btn2.classList.add('icon-btn');
|
|
166
|
+
const icon2 = document.createElement('oscd-icon');
|
|
167
|
+
icon2.textContent = 'bar';
|
|
168
|
+
btn2.appendChild(icon2);
|
|
169
|
+
|
|
170
|
+
root.appendChild(btn1);
|
|
171
|
+
root.appendChild(btn2);
|
|
172
|
+
|
|
173
|
+
const found = findButtonByIcon(root, '.icon-btn', 'bar');
|
|
174
|
+
expect(found).to.equal(btn2);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns null if no button matches the icon name', () => {
|
|
178
|
+
const root = document.createElement('div');
|
|
179
|
+
const btn = document.createElement('oscd-icon-button');
|
|
180
|
+
const icon = document.createElement('oscd-icon');
|
|
181
|
+
icon.textContent = 'foo';
|
|
182
|
+
btn.appendChild(icon);
|
|
183
|
+
root.appendChild(btn);
|
|
184
|
+
|
|
185
|
+
const found = findButtonByIcon(root, 'oscd-icon-button', 'bar');
|
|
186
|
+
expect(found).to.be.undefined;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns null if no button matches the selector', () => {
|
|
190
|
+
const root = document.createElement('div');
|
|
191
|
+
const btn = document.createElement('oscd-text-button');
|
|
192
|
+
const icon = document.createElement('oscd-icon');
|
|
193
|
+
icon.textContent = 'foo';
|
|
194
|
+
root.appendChild(btn);
|
|
195
|
+
|
|
196
|
+
const found = findButtonByIcon(root, 'oscd-icon-button', 'foo');
|
|
197
|
+
expect(found).to.be.undefined;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns null if oscd-icon is missing', () => {
|
|
201
|
+
const root = document.createElement('div');
|
|
202
|
+
const btn = document.createElement('oscd-text-button');
|
|
203
|
+
const icon = document.createElement('oscd-emoji');
|
|
204
|
+
icon.textContent = 'foo';
|
|
205
|
+
root.appendChild(btn);
|
|
206
|
+
|
|
207
|
+
const found = findButtonByIcon(root, 'oscd-text-button', 'foo');
|
|
208
|
+
expect(found).to.be.undefined;
|
|
209
|
+
});
|
|
210
|
+
});
|
package/utils/testing.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { OscdDialog } from '@omicronenergy/oscd-ui/dialog/oscd-dialog.js';
|
|
2
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
3
|
+
import { waitUntil } from '@open-wc/testing';
|
|
4
|
+
|
|
5
|
+
export function getFirstTextNodeContent(
|
|
6
|
+
element: Element | null,
|
|
7
|
+
): string | undefined {
|
|
8
|
+
if (!element) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
return Array.from(element.childNodes)
|
|
12
|
+
.filter(node => node.nodeType === Node.TEXT_NODE)
|
|
13
|
+
.map(node => node.textContent?.trim())
|
|
14
|
+
.filter(node => node !== '')[0];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function querySelectorContainingText(
|
|
18
|
+
scope: Element,
|
|
19
|
+
selector: string,
|
|
20
|
+
text: string,
|
|
21
|
+
) {
|
|
22
|
+
const elements = Array.from(scope.querySelectorAll(selector));
|
|
23
|
+
return elements.find(e => {
|
|
24
|
+
const textContent = getFirstTextNodeContent(e);
|
|
25
|
+
return textContent === text;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function simulateKeypressOnElement(key: string, ctrlKey: boolean) {
|
|
30
|
+
const event = new KeyboardEvent('keydown', {
|
|
31
|
+
key,
|
|
32
|
+
ctrlKey,
|
|
33
|
+
});
|
|
34
|
+
document.dispatchEvent(event);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function waitForDialogState(
|
|
38
|
+
dialog: Element,
|
|
39
|
+
state: 'open' | 'closed',
|
|
40
|
+
) {
|
|
41
|
+
await waitUntil(
|
|
42
|
+
() => (dialog as OscdDialog)?.open === (state === 'open'),
|
|
43
|
+
`Dialog did not ${state} within the expected time`,
|
|
44
|
+
);
|
|
45
|
+
return dialog;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Finds a control, typically a button (but in theory could be some other component) by selector + enclosed icon name (inside an oscd-icon).
|
|
50
|
+
* @param root The root element to search within.
|
|
51
|
+
* @param buttonSelector The selector (e.g. 'oscd-filled-icon-button').
|
|
52
|
+
* @param iconName The icon name to match (text content of enclosed oscd-icon).
|
|
53
|
+
* @returns The matching button element or null.
|
|
54
|
+
*/
|
|
55
|
+
export function findButtonByIcon(
|
|
56
|
+
root: ParentNode,
|
|
57
|
+
buttonSelector: string,
|
|
58
|
+
iconName: string,
|
|
59
|
+
): HTMLElement | null {
|
|
60
|
+
const buttons = Array.from(root.querySelectorAll(buttonSelector));
|
|
61
|
+
return buttons.find(btn => {
|
|
62
|
+
const icon = btn.querySelector('oscd-icon');
|
|
63
|
+
return icon && icon.textContent?.trim() === iconName;
|
|
64
|
+
}) as HTMLElement | null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
|
|
2
|
+
|
|
3
|
+
/** Use Hot Module replacement by adding --hmr to the start command */
|
|
4
|
+
const hmr = process.argv.includes('--hmr');
|
|
5
|
+
|
|
6
|
+
export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
|
|
7
|
+
open: '/demo/',
|
|
8
|
+
/** Use regular watch mode if HMR is not enabled. */
|
|
9
|
+
watch: !hmr,
|
|
10
|
+
/** Resolve bare module imports */
|
|
11
|
+
nodeResolve: {
|
|
12
|
+
exportConditions: ['browser', 'development'],
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
|
|
16
|
+
// esbuildTarget: 'auto'
|
|
17
|
+
|
|
18
|
+
plugins: [
|
|
19
|
+
/** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
|
|
20
|
+
// hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
// See documentation for all available options
|
|
24
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { visualRegressionPlugin } from '@web/test-runner-visual-regression/plugin';
|
|
2
|
+
import { playwrightLauncher } from '@web/test-runner-playwright';
|
|
3
|
+
|
|
4
|
+
import pixelmatch from 'pixelmatch';
|
|
5
|
+
import { PNG } from 'pngjs';
|
|
6
|
+
|
|
7
|
+
const fuzzy = ['win32', 'darwin'].includes(process.platform); // allow for 1% difference on non-linux OSs
|
|
8
|
+
const local = !process.env.CI;
|
|
9
|
+
|
|
10
|
+
console.assert(local, 'Running in CI!');
|
|
11
|
+
console.assert(!fuzzy, 'Running on OS with 1% test pixel diff threshold!');
|
|
12
|
+
|
|
13
|
+
const thresholdPercentage = fuzzy && local ? 1 : 0;
|
|
14
|
+
|
|
15
|
+
const filteredLogs = [
|
|
16
|
+
'Running in dev mode',
|
|
17
|
+
'Lit is in dev mode',
|
|
18
|
+
'scheduled an update',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const browsers = [
|
|
22
|
+
playwrightLauncher({ product: 'chromium' }),
|
|
23
|
+
playwrightLauncher({ product: 'firefox' }),
|
|
24
|
+
playwrightLauncher({ product: 'webkit' }),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function defaultGetImageDiff({ baselineImage, image, options }) {
|
|
28
|
+
let error = '';
|
|
29
|
+
let basePng = PNG.sync.read(baselineImage);
|
|
30
|
+
let png = PNG.sync.read(image);
|
|
31
|
+
let { width, height } = png;
|
|
32
|
+
|
|
33
|
+
if (basePng.width !== png.width || basePng.height !== png.height) {
|
|
34
|
+
error =
|
|
35
|
+
`Screenshot is not the same width and height as the baseline. ` +
|
|
36
|
+
`Baseline: { width: ${basePng.width}, height: ${basePng.height} }` +
|
|
37
|
+
`Screenshot: { width: ${png.width}, height: ${png.height} }`;
|
|
38
|
+
width = Math.max(basePng.width, png.width);
|
|
39
|
+
height = Math.max(basePng.height, png.height);
|
|
40
|
+
let oldPng = basePng;
|
|
41
|
+
basePng = new PNG({ width, height });
|
|
42
|
+
oldPng.data.copy(basePng.data, 0, 0, oldPng.data.length);
|
|
43
|
+
oldPng = png;
|
|
44
|
+
png = new PNG({ width, height });
|
|
45
|
+
oldPng.data.copy(png.data, 0, 0, oldPng.data.length);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const diff = new PNG({ width, height });
|
|
49
|
+
|
|
50
|
+
const numDiffPixels = pixelmatch(
|
|
51
|
+
basePng.data,
|
|
52
|
+
png.data,
|
|
53
|
+
diff.data,
|
|
54
|
+
width,
|
|
55
|
+
height,
|
|
56
|
+
options,
|
|
57
|
+
);
|
|
58
|
+
const diffPercentage = (numDiffPixels / (width * height)) * 100;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
error,
|
|
62
|
+
diffImage: PNG.sync.write(diff),
|
|
63
|
+
diffPercentage,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
|
|
68
|
+
plugins: [
|
|
69
|
+
visualRegressionPlugin({
|
|
70
|
+
update: process.argv.includes('--update-visual-baseline'),
|
|
71
|
+
getImageDiff: options => {
|
|
72
|
+
const result = defaultGetImageDiff(options);
|
|
73
|
+
if (result.diffPercentage < thresholdPercentage) {
|
|
74
|
+
result.diffPercentage = 0;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
files: 'dist/**/*.spec.js',
|
|
82
|
+
|
|
83
|
+
coverageConfig: {
|
|
84
|
+
exclude: [
|
|
85
|
+
'**/node_modules/**',
|
|
86
|
+
'**/__wds-outside-root__/**', // Exclude external modules like oscd-ui
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
groups: [
|
|
91
|
+
{
|
|
92
|
+
name: 'visual',
|
|
93
|
+
files: 'dist/**/*.test.js',
|
|
94
|
+
testRunnerHtml: testFramework => `
|
|
95
|
+
<!DOCTYPE html>
|
|
96
|
+
<html>
|
|
97
|
+
<head>
|
|
98
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300&family=Roboto:wght@300;400;500&display=swap" >
|
|
99
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" >
|
|
100
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" >
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<style class="deanimator">
|
|
104
|
+
*, *::before, *::after {
|
|
105
|
+
-moz-transition: none !important;
|
|
106
|
+
transition: none !important;
|
|
107
|
+
-moz-animation: none !important;
|
|
108
|
+
animation: none !important;
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
111
|
+
<script>window.process = { env: ${JSON.stringify(process.env)} }</script>
|
|
112
|
+
<script type="module" src="${testFramework}"></script>
|
|
113
|
+
<script>
|
|
114
|
+
function descendants(parent) {
|
|
115
|
+
return (Array.from(parent.childNodes)).concat(
|
|
116
|
+
...Array.from(parent.children).map(child => descendants(child))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const deanimator = document.querySelector('.deanimator');
|
|
120
|
+
function deanimate(element) {
|
|
121
|
+
if (!element.shadowRoot) return;
|
|
122
|
+
if (element.shadowRoot.querySelector('.deanimator')) return;
|
|
123
|
+
const style = deanimator.cloneNode(true);
|
|
124
|
+
element.shadowRoot.appendChild(style);
|
|
125
|
+
descendants(element.shadowRoot).forEach(deanimate);
|
|
126
|
+
}
|
|
127
|
+
const observer = new MutationObserver((mutationList, observer) => {
|
|
128
|
+
for (const mutation of mutationList) {
|
|
129
|
+
if (mutation.type === 'childList') {
|
|
130
|
+
descendants(document.body).forEach(deanimate);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
observer.observe(document.body, {childList: true, subtree:true});
|
|
135
|
+
</script>
|
|
136
|
+
<style>
|
|
137
|
+
* {
|
|
138
|
+
margin: 0px;
|
|
139
|
+
padding: 0px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
oscd-shell {
|
|
143
|
+
display: block;
|
|
144
|
+
height: 600px;
|
|
145
|
+
}
|
|
146
|
+
</style>
|
|
147
|
+
</body>
|
|
148
|
+
</html>`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
|
|
152
|
+
/** Resolve bare module imports */
|
|
153
|
+
nodeResolve: {
|
|
154
|
+
exportConditions: ['browser', 'development'],
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/** Filter out lit dev mode logs */
|
|
158
|
+
filterBrowserLogs(log) {
|
|
159
|
+
for (const arg of log.args) {
|
|
160
|
+
if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
|
|
168
|
+
// esbuildTarget: 'auto',
|
|
169
|
+
|
|
170
|
+
/** Amount of browsers to run concurrently */
|
|
171
|
+
concurrentBrowsers: 3,
|
|
172
|
+
|
|
173
|
+
/** Amount of test files per browser to test concurrently */
|
|
174
|
+
concurrency: 1,
|
|
175
|
+
|
|
176
|
+
/** Browsers to run tests on */
|
|
177
|
+
browsers,
|
|
178
|
+
|
|
179
|
+
// See documentation for all available options
|
|
180
|
+
});
|