Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/scoped registries improvements #2407

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/empty-students-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ui': patch
---

no registration of same class twice w/o scoped-registries polyfill
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"custom-elements-manifest": "npm run custom-elements-manifest --workspaces --if-present && node ./scripts/create-api-tables.mjs",
"debug": "web-test-runner --watch --config web-test-runner-chrome.config.mjs",
"debug:firefox": "web-test-runner --watch --config web-test-runner-firefox.config.mjs",
"debug:no-scoped-registries-polyfill": "npm run debug -- --no-scoped-registries-polyfill",
Copy link
Collaborator

@okadurin okadurin Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add both with polyfill and without polyfill configs to test:browser so that the npm run test covers both cases? Or is it to be done in a separate PR in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that as well, but for now I kept it limited. Feel free to do a proposal in the future

"debug:webkit": "web-test-runner --watch --config web-test-runner-webkit.config.mjs",
"format": "npm run format:eslint && npm run format:prettier",
"format:eslint": "eslint --ext .js,.html . --fix",
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/components/core/src/ScopedElementsMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ const ScopedElementsMixinImplementation = superclass =>
*/
defineScopedElement(tagName, classToBeRegistered) {
const registeredClass = this.registry.get(tagName);
const isAlreadyRegistered = registeredClass && registeredClass === classToBeRegistered;
if (isAlreadyRegistered && !supportsScopedRegistry()) {
const isNewClassWithSameName = registeredClass && registeredClass !== classToBeRegistered;
if (!supportsScopedRegistry() && isNewClassWithSameName) {
// eslint-disable-next-line no-console
console.error(
[
Expand Down
35 changes: 34 additions & 1 deletion packages/ui/components/core/test/ScopedElementsMixin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
} from '@lit-labs/testing/fixtures.js';
import { LitElement, html } from 'lit';
import sinon from 'sinon';
import { browserDetection } from '../src/browserDetection.js';

import { ScopedElementsMixin, supportsScopedRegistry } from '../src/ScopedElementsMixin.js';
import { browserDetection } from '../src/browserDetection.js';

const hasRealScopedRegistrySupport = supportsScopedRegistry();
const originalShadowRootProps = {
Expand Down Expand Up @@ -78,6 +78,8 @@ describe('ScopedElementsMixin', () => {

describe('When scoped registries are supported', () => {
it('registers elements on local registry', async () => {
if (!hasRealScopedRegistrySupport) return;

const ceDefineSpy = sinon.spy(customElements, 'define');

const el = /** @type {ScopedElementsHost} */ (
Expand Down Expand Up @@ -127,5 +129,36 @@ describe('ScopedElementsMixin', () => {
expect(ceDefineSpy.calledWith('scoped-elements-child-no-reg')).to.be.true;
ceDefineSpy.restore();
});

it('fails when different classes are registered under different name', async () => {
class ScopedElementsHostNoReg2 extends ScopedElementsMixin(LitElement) {
static scopedElements = { 'scoped-elements-child-no-reg': class extends HTMLElement {} };

render() {
return html`<scoped-elements-child-no-reg></scoped-elements-child-no-reg>`;
}
}
customElements.define('scoped-elements-host-no-reg-2', ScopedElementsHostNoReg2);

const errorSpy = sinon.spy(console, 'error');
/** @type {ScopedElementsHostNoReg2} */ (
await fixture(html`<scoped-elements-host-no-reg></scoped-elements-host-no-reg>`)
);
/** @type {ScopedElementsHostNoReg2} */ (
await fixture(html`<scoped-elements-host-no-reg-2></scoped-elements-host-no-reg-2>`)
);

expect(errorSpy.args[0][0]).to.equal(
[
'You are trying to re-register the "scoped-elements-child-no-reg" custom element with a different class via ScopedElementsMixin.',
'This is only possible with a CustomElementRegistry.',
'Your browser does not support this feature so you will need to load a polyfill for it.',
'Load "@webcomponents/scoped-custom-element-registry" before you register ANY web component to the global customElements registry.',
'e.g. add "<script src="/node_modules/@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js"></script>" as your first script tag.',
'For more details you can visit https://open-wc.org/docs/development/scoped-elements/',
].join('\n'),
);
errorSpy.restore();
});
});
});
41 changes: 13 additions & 28 deletions packages/ui/components/core/test/SlotMixin.test.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
import sinon from 'sinon';
import { defineCE, expect, fixture, fixtureSync, unsafeStatic, html } from '@open-wc/testing';
import { ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
import { SlotMixin } from '@lion/ui/core.js';
import { LitElement } from 'lit';
import sinon from 'sinon';

import { ScopedElementsMixin, supportsScopedRegistry } from '../src/ScopedElementsMixin.js';

/**
* @typedef {import('../types/SlotMixinTypes.js').SlotHost} SlotHost
*/

// @ts-ignore
const createElementNative = ShadowRoot.prototype.createElement;
function mockScopedRegistry() {
const outputObj = { createElementCallCount: 0 };
// @ts-expect-error wait for browser support
ShadowRoot.prototype.createElement = (tagName, options) => {
outputObj.createElementCallCount += 1;
// Return an element that lit can use as render target
return createElementNative(tagName, options);
};
return outputObj;
}

function unMockScopedRegistry() {
// @ts-expect-error wait for browser support
ShadowRoot.prototype.createElement = createElementNative;
}

describe('SlotMixin', () => {
it('inserts provided element into light dom and sets slot', async () => {
const tag = defineCE(
Expand Down Expand Up @@ -568,8 +551,11 @@ describe('SlotMixin', () => {
});

describe('Scoped Registries', () => {
it('supports scoped elements when polyfill loaded', async () => {
const outputObj = mockScopedRegistry();
it('supports scoped elements when scoped registries supported (or polyfill loaded)', async () => {
if (!supportsScopedRegistry()) return;

// @ts-expect-error
const createElSpy = sinon.spy(ShadowRoot.prototype, 'createElement');

class ScopedEl extends LitElement {}

Expand Down Expand Up @@ -600,14 +586,14 @@ describe('SlotMixin', () => {
const tag = unsafeStatic(tagName);
await fixture(html`<${tag}></${tag}>`);

expect(outputObj.createElementCallCount).to.equal(1);
expect(createElSpy.callCount).to.equal(1);

unMockScopedRegistry();
createElSpy.restore();
});

it('does not scope elements when polyfill not loaded', async () => {
// @ts-expect-error
ShadowRoot.prototype.createElement = null;
it('does not scope elements when scoped registries not supported (or polyfill not loaded)', async () => {
if (supportsScopedRegistry()) return;

class ScopedEl extends LitElement {}

const tagName = defineCE(
Expand Down Expand Up @@ -645,7 +631,6 @@ describe('SlotMixin', () => {

document.body.removeChild(renderTarget);
docSpy.restore();
unMockScopedRegistry();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import { LitElement } from 'lit';
import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import {
getFormControlMembers,
AlwaysInvalid,
AlwaysValid,
AsyncAlwaysInvalid,
AsyncAlwaysValid,
AlwaysInvalid,
AlwaysValid,
} from '@lion/ui/form-core-test-helpers.js';
import sinon from 'sinon';
import {
ResultValidator,
ValidateMixin,
EqualsLength,
Unparseable,
MaxLength,
MinLength,
Required,
ResultValidator,
Unparseable,
ValidateMixin,
Validator,
Required,
} from '@lion/ui/form-core.js';

import '@lion/ui/define/lion-field.js';
import '@lion/ui/define/lion-validation-feedback.js';
import '@lion/ui/define/lion-field.js';

/**
* @typedef {import('@lion/ui/form-core.js').LionField} LionField
Expand Down
52 changes: 17 additions & 35 deletions web-test-runner.config.mjs
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
import fs from 'fs';
import { playwrightLauncher } from '@web/test-runner-playwright';
import { litSsrPlugin } from '@lit-labs/testing/web-test-runner-ssr-plugin.js';
import { playwrightLauncher } from '@web/test-runner-playwright';
import { optimisedGlob } from 'providence-analytics/utils.js';

const devMode = process.argv.includes('--dev-mode');
const config = {
shouldLoadPolyfill: !process.argv.includes('--no-scoped-registries-polyfill'),
shouldRunDevMode: process.argv.includes('--dev-mode'),
};

const packages = fs
.readdirSync('packages')
.filter(
dir => fs.statSync(`packages/${dir}`).isDirectory() && fs.existsSync(`packages/${dir}/test`),
)
.map(dir => ({ name: dir, path: `packages/${dir}/test` }))
.concat(
fs
.readdirSync('packages/ui/components')
.filter(
dir =>
fs.statSync(`packages/ui/components/${dir}`).isDirectory() &&
fs.existsSync(`packages/ui/components/${dir}/test`),
)
.map(dir => ({ name: dir, path: `packages/ui/components/${dir}/test` })),
);
const groups = (
await optimisedGlob(['packages/*/test', 'packages/ui/components/**/test'], {
onlyDirectories: true,
})
).map(dir => ({ name: dir.split('/').at(-2), files: `${dir}/**/*.test.js` }));

/**
* @type {import('@web/test-runner').TestRunnerConfig['testRunnerHtml']}
*/
const testRunnerHtml = testRunnerImport =>
const testRunnerHtmlWithPolyfill = testRunnerImport =>
`
<html>
<head>
Expand All @@ -35,31 +27,21 @@ const testRunnerHtml = testRunnerImport =>
`;

export default {
nodeResolve: { exportConditions: [devMode && 'development'] },
nodeResolve: config.shouldRunDevMode ? { exportConditions: ['development'] } : true,
coverageConfig: {
report: true,
reportDir: 'coverage',
threshold: {
statements: 95,
functions: 95,
branches: 95,
lines: 95,
},
threshold: { statements: 95, functions: 95, branches: 95, lines: 95 },
},
testFramework: {
config: {
timeout: '5000',
},
config: { timeout: '5000' },
},
testRunnerHtml,
testRunnerHtml: config.shouldLoadPolyfill ? testRunnerHtmlWithPolyfill : undefined,
browsers: [
playwrightLauncher({ product: 'firefox', concurrency: 1 }),
playwrightLauncher({ product: 'chromium' }),
playwrightLauncher({ product: 'webkit' }),
],
groups: packages.map(pkg => ({
name: pkg.name,
files: `${pkg.path}/**/*.test.js`,
})),
groups,
plugins: [litSsrPlugin()],
};