diff --git a/packages/@lwc/integration-not-karma/configs/base.mjs b/packages/@lwc/integration-not-karma/configs/base.js similarity index 92% rename from packages/@lwc/integration-not-karma/configs/base.mjs rename to packages/@lwc/integration-not-karma/configs/base.js index 1cca526416..4b08ac144f 100644 --- a/packages/@lwc/integration-not-karma/configs/base.mjs +++ b/packages/@lwc/integration-not-karma/configs/base.js @@ -1,7 +1,7 @@ import { join } from 'node:path'; import { LWC_VERSION } from '@lwc/shared'; import { importMapsPlugin } from '@web/dev-server-import-maps'; -import * as options from '../helpers/options.mjs'; +import * as options from '../helpers/options.js'; const pluck = (obj, keys) => Object.fromEntries(keys.map((k) => [k, obj[k]])); const maybeImport = (file, condition) => (condition ? `await import('${file}');` : ''); @@ -29,14 +29,15 @@ export default { // time out before they receive focus. But it also makes the full suite take 3x longer to run... // Potential workaround: https://github.com/modernweb-dev/web/issues/2588 concurrency: 1, + filterBrowserLogs: () => false, nodeResolve: true, rootDir: join(import.meta.dirname, '..'), plugins: [ - importMapsPlugin({ inject: { importMap: { imports: { lwc: './mocks/lwc.mjs' } } } }), + importMapsPlugin({ inject: { importMap: { imports: { lwc: './mocks/lwc.js' } } } }), { resolveImport({ source }) { if (source === 'test-utils') { - return '/helpers/utils.mjs'; + return '/helpers/utils.js'; } else if (source === 'wire-service') { // To serve files outside the web root (e.g. node_modules in the monorepo root), // @web/dev-server provides this "magic" path. It's hacky of us to use it directly. @@ -66,7 +67,7 @@ export default { ${maybeImport('@lwc/synthetic-shadow', !options.DISABLE_SYNTHETIC)} ${maybeImport('@lwc/aria-reflection', options.ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL)} - + `, diff --git a/packages/@lwc/integration-not-karma/configs/hydration.mjs b/packages/@lwc/integration-not-karma/configs/hydration.js similarity index 88% rename from packages/@lwc/integration-not-karma/configs/hydration.mjs rename to packages/@lwc/integration-not-karma/configs/hydration.js index eb81e7701a..547844d317 100644 --- a/packages/@lwc/integration-not-karma/configs/hydration.mjs +++ b/packages/@lwc/integration-not-karma/configs/hydration.js @@ -1,7 +1,7 @@ // Use native shadow by default in hydration tests; MUST be set before imports process.env.DISABLE_SYNTHETIC ??= 'true'; -import baseConfig from './base.mjs'; -import hydrationTestPlugin from './plugins/serve-hydration.mjs'; +import baseConfig from './base.js'; +import hydrationTestPlugin from './plugins/serve-hydration.js'; /** @type {import("@web/test-runner").TestRunnerConfig} */ export default { diff --git a/packages/@lwc/integration-not-karma/configs/integration.mjs b/packages/@lwc/integration-not-karma/configs/integration.js similarity index 86% rename from packages/@lwc/integration-not-karma/configs/integration.mjs rename to packages/@lwc/integration-not-karma/configs/integration.js index 3479e788d4..fe76eab25a 100644 --- a/packages/@lwc/integration-not-karma/configs/integration.mjs +++ b/packages/@lwc/integration-not-karma/configs/integration.js @@ -1,5 +1,5 @@ -import baseConfig from './base.mjs'; -import testPlugin from './plugins/serve-integration.mjs'; +import baseConfig from './base.js'; +import testPlugin from './plugins/serve-integration.js'; /** @type {import("@web/test-runner").TestRunnerConfig} */ export default { diff --git a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.mjs b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js similarity index 99% rename from packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.mjs rename to packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js index e89b764b68..ebb724d9a2 100644 --- a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.mjs +++ b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js @@ -3,7 +3,7 @@ import vm from 'node:vm'; import fs from 'node:fs/promises'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; -import { DISABLE_STATIC_CONTENT_OPTIMIZATION, ENGINE_SERVER } from '../../helpers/options.mjs'; +import { DISABLE_STATIC_CONTENT_OPTIMIZATION, ENGINE_SERVER } from '../../helpers/options.js'; const lwcSsr = await (ENGINE_SERVER ? import('@lwc/engine-server') : import('@lwc/ssr-runtime')); const ROOT_DIR = path.join(import.meta.dirname, '../..'); diff --git a/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.mjs b/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js similarity index 99% rename from packages/@lwc/integration-not-karma/configs/plugins/serve-integration.mjs rename to packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js index c59a990436..6604e74d08 100644 --- a/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.mjs +++ b/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js @@ -7,7 +7,7 @@ import { COVERAGE, DISABLE_STATIC_CONTENT_OPTIMIZATION, DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER, -} from '../../helpers/options.mjs'; +} from '../../helpers/options.js'; /** Cache reused between each compilation to speed up the compilation time. */ let cache; diff --git a/packages/@lwc/integration-not-karma/helpers/aria.mjs b/packages/@lwc/integration-not-karma/helpers/aria.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/aria.mjs rename to packages/@lwc/integration-not-karma/helpers/aria.js diff --git a/packages/@lwc/integration-not-karma/helpers/console.mjs b/packages/@lwc/integration-not-karma/helpers/console.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/console.mjs rename to packages/@lwc/integration-not-karma/helpers/console.js diff --git a/packages/@lwc/integration-not-karma/helpers/constants.mjs b/packages/@lwc/integration-not-karma/helpers/constants.js similarity index 93% rename from packages/@lwc/integration-not-karma/helpers/constants.mjs rename to packages/@lwc/integration-not-karma/helpers/constants.js index 0dac9715b3..25c89c3116 100644 --- a/packages/@lwc/integration-not-karma/helpers/constants.mjs +++ b/packages/@lwc/integration-not-karma/helpers/constants.js @@ -1,4 +1,4 @@ -import { API_VERSION } from './options.mjs'; +import { API_VERSION } from './options.js'; // These values are based on the API versions in @lwc/shared/api-version export const LOWERCASE_SCOPE_TOKENS = API_VERSION >= 59, diff --git a/packages/@lwc/integration-not-karma/helpers/hooks.mjs b/packages/@lwc/integration-not-karma/helpers/hooks.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/hooks.mjs rename to packages/@lwc/integration-not-karma/helpers/hooks.js diff --git a/packages/@lwc/integration-not-karma/helpers/matchers/console.mjs b/packages/@lwc/integration-not-karma/helpers/matchers/console.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/matchers/console.mjs rename to packages/@lwc/integration-not-karma/helpers/matchers/console.js diff --git a/packages/@lwc/integration-not-karma/helpers/matchers/errors.mjs b/packages/@lwc/integration-not-karma/helpers/matchers/errors.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/matchers/errors.mjs rename to packages/@lwc/integration-not-karma/helpers/matchers/errors.js diff --git a/packages/@lwc/integration-not-karma/helpers/matchers/index.mjs b/packages/@lwc/integration-not-karma/helpers/matchers/index.js similarity index 52% rename from packages/@lwc/integration-not-karma/helpers/matchers/index.mjs rename to packages/@lwc/integration-not-karma/helpers/matchers/index.js index 95397e4e1d..a917e54668 100644 --- a/packages/@lwc/integration-not-karma/helpers/matchers/index.mjs +++ b/packages/@lwc/integration-not-karma/helpers/matchers/index.js @@ -1,6 +1,6 @@ -import { registerConsoleMatchers } from './console.mjs'; -import { registerErrorMatchers } from './errors.mjs'; -import { registerJasmineMatchers } from './jasmine.mjs'; +import { registerConsoleMatchers } from './console.js'; +import { registerErrorMatchers } from './errors.js'; +import { registerJasmineMatchers } from './jasmine.js'; export const registerCustomMatchers = (chai, utils) => { registerConsoleMatchers(chai, utils); diff --git a/packages/@lwc/integration-not-karma/helpers/matchers/jasmine.mjs b/packages/@lwc/integration-not-karma/helpers/matchers/jasmine.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/matchers/jasmine.mjs rename to packages/@lwc/integration-not-karma/helpers/matchers/jasmine.js diff --git a/packages/@lwc/integration-not-karma/helpers/options.mjs b/packages/@lwc/integration-not-karma/helpers/options.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/options.mjs rename to packages/@lwc/integration-not-karma/helpers/options.js diff --git a/packages/@lwc/integration-not-karma/helpers/setup.mjs b/packages/@lwc/integration-not-karma/helpers/setup.js similarity index 97% rename from packages/@lwc/integration-not-karma/helpers/setup.mjs rename to packages/@lwc/integration-not-karma/helpers/setup.js index 418e166ebc..7a64aff7cb 100644 --- a/packages/@lwc/integration-not-karma/helpers/setup.mjs +++ b/packages/@lwc/integration-not-karma/helpers/setup.js @@ -3,8 +3,8 @@ import { JestAsymmetricMatchers, JestChaiExpect, JestExtend } from '@vitest/expe import * as chai from 'chai'; import * as LWC from 'lwc'; import { spyOn, fn } from '@vitest/spy'; -import { registerCustomMatchers } from './matchers/index.mjs'; -import * as TestUtils from './utils.mjs'; +import { registerCustomMatchers } from './matchers/index.js'; +import * as TestUtils from './utils.js'; // FIXME: As a relic of the Karma tests, some test files rely on the global object, // rather than importing from `test-utils`. diff --git a/packages/@lwc/integration-not-karma/helpers/signals.mjs b/packages/@lwc/integration-not-karma/helpers/signals.js similarity index 100% rename from packages/@lwc/integration-not-karma/helpers/signals.mjs rename to packages/@lwc/integration-not-karma/helpers/signals.js diff --git a/packages/@lwc/integration-not-karma/helpers/utils.mjs b/packages/@lwc/integration-not-karma/helpers/utils.js similarity index 98% rename from packages/@lwc/integration-not-karma/helpers/utils.mjs rename to packages/@lwc/integration-not-karma/helpers/utils.js index 5ebdd95dde..3c31c07047 100644 --- a/packages/@lwc/integration-not-karma/helpers/utils.mjs +++ b/packages/@lwc/integration-not-karma/helpers/utils.js @@ -8,9 +8,9 @@ import { ariaPropertiesMapping, nonPolyfilledAriaProperties, nonStandardAriaProperties, -} from './aria.mjs'; -import { setHooks, getHooks } from './hooks.mjs'; -import { spyConsole } from './console.mjs'; +} from './aria.js'; +import { setHooks, getHooks } from './hooks.js'; +import { spyConsole } from './console.js'; import { DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, ENABLE_ELEMENT_INTERNALS_AND_FACE, @@ -22,8 +22,8 @@ import { USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, USE_LIGHT_DOM_SLOT_FORWARDING, -} from './constants.mjs'; -import { addTrustedSignal } from './signals.mjs'; +} from './constants.js'; +import { addTrustedSignal } from './signals.js'; // Listen for errors thrown directly by the callback function directErrorListener(callback) { diff --git a/packages/@lwc/integration-not-karma/mocks/lwc.js b/packages/@lwc/integration-not-karma/mocks/lwc.js new file mode 100644 index 0000000000..023844a214 --- /dev/null +++ b/packages/@lwc/integration-not-karma/mocks/lwc.js @@ -0,0 +1,6 @@ +// IMPORTANT: we must use @lwc/engine-dom instead of lwc in order to avoid circular imports +import { sanitizeAttribute as _sanitizeAttribute } from '@lwc/engine-dom'; +import { fn } from '@vitest/spy'; + +export * from '@lwc/engine-dom'; +export const sanitizeAttribute = fn(_sanitizeAttribute); diff --git a/packages/@lwc/integration-not-karma/package.json b/packages/@lwc/integration-not-karma/package.json index d05352ae3f..7858140d83 100644 --- a/packages/@lwc/integration-not-karma/package.json +++ b/packages/@lwc/integration-not-karma/package.json @@ -2,10 +2,11 @@ "name": "@lwc/integration-not-karma", "private": true, "version": "8.21.6", + "type": "module", "scripts": { "start": "web-test-runner --manual", - "test": "web-test-runner --config configs/integration.mjs", - "test:hydration": "web-test-runner --config configs/hydration.mjs" + "test": "web-test-runner --config configs/integration.js", + "test:hydration": "web-test-runner --config configs/hydration.js" }, "devDependencies": { "@lwc/compiler": "8.21.6", diff --git a/packages/@lwc/integration-not-karma/test b/packages/@lwc/integration-not-karma/test deleted file mode 120000 index ecd98dd932..0000000000 --- a/packages/@lwc/integration-not-karma/test +++ /dev/null @@ -1 +0,0 @@ -../integration-karma/test \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration b/packages/@lwc/integration-not-karma/test-hydration deleted file mode 120000 index afa14987d3..0000000000 --- a/packages/@lwc/integration-not-karma/test-hydration +++ /dev/null @@ -1 +0,0 @@ -../integration-karma/test-hydration \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/index.spec.js new file mode 100644 index 0000000000..d8fda70ab3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/index.spec.js @@ -0,0 +1,16 @@ +export default { + snapshot(target) { + const span = target.shadowRoot.querySelector('span'); + + return { + span, + }; + }, + test(elm, snapshot, consoleCalls) { + const span = elm.shadowRoot.querySelector('span'); + expect(span).toBe(snapshot.span); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/x/main/main.html new file mode 100644 index 0000000000..2f36aeb6a2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/x/main/main.js new file mode 100644 index 0000000000..f4cfc750df --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-and-comment-nodes/x/main/main.js @@ -0,0 +1,6 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + foo = ''; + bar = ''; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/index.spec.js new file mode 100644 index 0000000000..caa8b5e53f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/index.spec.js @@ -0,0 +1,34 @@ +export default { + props: {}, + snapshot(target) { + const first = target.shadowRoot.querySelector('.first'); + const second = target.shadowRoot.querySelector('.second'); + + return { + first, + second, + }; + }, + advancedTest(target, { Component, hydrateComponent, consoleSpy, container, selector }) { + const snapshotBeforeHydration = this.snapshot(target); + hydrateComponent(target, Component, this.props); + const hydratedTarget = container.querySelector(selector); + const snapshotAfterHydration = this.snapshot(hydratedTarget); + + for (const snapshotKey of Object.keys(snapshotBeforeHydration)) { + expect(snapshotBeforeHydration[snapshotKey]) + .withContext( + `${snapshotKey} should be the same DOM element both before and after hydration` + ) + .toBe(snapshotAfterHydration[snapshotKey]); + expect(snapshotBeforeHydration[snapshotKey].childNodes) + .withContext( + `${snapshotKey} should have the same number of child nodes before & after hydration` + ) + .toHaveSize(snapshotAfterHydration[snapshotKey].childNodes.length); + } + + expect(consoleSpy.calls.warn).toHaveSize(0); + expect(consoleSpy.calls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/x/main/main.html new file mode 100644 index 0000000000..da544e5d42 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/x/main/main.js new file mode 100644 index 0000000000..290f646ebd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/adjacent-text-nodes/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + zeroLengthText = ''; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/class/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/class/index.spec.js new file mode 100644 index 0000000000..54eeb5738f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/class/index.spec.js @@ -0,0 +1,14 @@ +export default { + snapshot(target) { + const div = target.shadowRoot.querySelector('div'); + return { + div, + class: div.getAttribute('class'), + }; + }, + test(target, snapshots) { + const div = target.shadowRoot.querySelector('div'); + expect(div).toBe(snapshots.div); + expect(div.getAttribute('class')).toBe(snapshots.class); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/class/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/class/x/main/main.html new file mode 100644 index 0000000000..eb0c5a0bff --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/class/x/main/main.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/class/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/class/x/main/main.js new file mode 100644 index 0000000000..ebaaa40b3a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/class/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + className = 'default_class'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/index.spec.js new file mode 100644 index 0000000000..5eab1e2f9d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/index.spec.js @@ -0,0 +1,17 @@ +export default { + clientProps: { + foo: 'foo', + }, + snapshot(target) { + const div = target.shadowRoot.querySelector('div'); + return { + div, + foo: div.getAttribute('data-foo'), + }; + }, + test(target, snapshots) { + const div = target.shadowRoot.querySelector('div'); + expect(div).toBe(snapshots.div); + expect(div.getAttribute('data-foo')).toBe(snapshots.foo); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/x/main/main.html new file mode 100644 index 0000000000..3d8e7daedc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/x/main/main.js new file mode 100644 index 0000000000..3d2456f1a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/expression/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + foo = 'foo'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/index.spec.js new file mode 100644 index 0000000000..fc730181aa --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/index.spec.js @@ -0,0 +1,40 @@ +export default { + props: { + isFalse: false, + isUndefined: undefined, + isNull: null, + isTrue: true, + isEmptyString: '', + isZero: 0, + isNaN: NaN, + }, + clientProps: { + isFalse: 'false', + isUndefined: 'undefined', // mismatch. should be literally `null`, not the string `"undefined"` + isNull: 'null', // mismatch. should be literally `null`, not the string `"null"` + isTrue: 'true', + isEmptyString: '', + isZero: '0', + isNaN: 'NaN', + }, + test(target, snapshots, consoleCalls) { + const divs = target.shadowRoot.querySelectorAll('div'); + + const expectedAttrValues = ['false', 'undefined', 'null', 'true', '', '0', 'NaN']; + + expect(divs).toHaveSize(expectedAttrValues.length); + + for (let i = 0; i < expectedAttrValues.length; i++) { + expect(divs[i].getAttribute('data-foo')).toEqual(expectedAttrValues[i]); + } + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:
- rendered on server: data-foo=null - expected on client: data-foo="undefined"', + 'Hydration attribute mismatch on:
- rendered on server: data-foo=null - expected on client: data-foo="null"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/x/main/main.html new file mode 100644 index 0000000000..bedcc80b4c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/x/main/main.html @@ -0,0 +1,9 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/x/main/main.js new file mode 100644 index 0000000000..081d21ef51 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/x/main/main.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api isFalse; + @api isUndefined; + @api isNull; + @api isTrue; + @api isEmptyString; + @api isZero; + @api isNaN; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/index.spec.js new file mode 100644 index 0000000000..020ab12b29 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/index.spec.js @@ -0,0 +1,16 @@ +export default { + test(target, snapshots, consoleCalls) { + const divs = target.shadowRoot.querySelectorAll('div'); + + const expectedAttrValues = ['false', null, null, 'true', '', '0', 'NaN']; + + expect(divs).toHaveSize(expectedAttrValues.length); + + for (let i = 0; i < expectedAttrValues.length; i++) { + expect(divs[i].getAttribute('data-foo')).toEqual(expectedAttrValues[i]); + } + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/x/main/main.html new file mode 100644 index 0000000000..bedcc80b4c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/x/main/main.html @@ -0,0 +1,9 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/x/main/main.js new file mode 100644 index 0000000000..e5674e3a6a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy/x/main/main.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + isFalse = false; + isUndefined = undefined; + isNull = null; + isTrue = true; + isEmptyString = ''; + isZero = 0; + isNaN = NaN; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/index.spec.js new file mode 100644 index 0000000000..0269a7bc78 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/index.spec.js @@ -0,0 +1,36 @@ +function getAllDivs(target) { + const childs = [...target.shadowRoot.querySelectorAll('x-child')]; + return childs.flatMap((child) => [...child.shadowRoot.querySelectorAll('div')]); +} +export default { + snapshot(target) { + const divs = getAllDivs(target); + return { divs }; + }, + test(target, snapshots, consoleCalls) { + const divs = getAllDivs(target); + expect(divs.length).toBe(snapshots.divs.length); + // dynamic + expect(divs[0].textContent).toBe('id: parentProvided'); + expect(divs[1].textContent).toBe('draggable: true'); + expect(divs[2].textContent).toBe('hidden: true'); + expect(divs[3].textContent).toBe('spellcheck: true'); + expect(divs[4].textContent).toBe('tabindex: -1'); + // static + expect(divs[5].textContent).toBe('id: parentProvided'); + expect(divs[6].textContent).toBe('draggable: true'); + expect(divs[7].textContent).toBe('hidden: true'); + expect(divs[8].textContent).toBe('spellcheck: true'); + expect(divs[9].textContent).toBe('tabindex: -1'); + + /** + * Required because SSR V1 is wrong (parent does not override child) and this results in hydration errors + */ + if (process.env.ENGINE_SERVER && process.env.NODE_ENV !== 'production') { + expect(consoleCalls.warn.toString()).toContain('Hydration text content mismatch'); + } else { + expect(consoleCalls.warn).toHaveSize(0); + } + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/child/child.html new file mode 100644 index 0000000000..2b60d98ba1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/child/child.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/child/child.js new file mode 100644 index 0000000000..ec210cc80c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/child/child.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + // None of these values should be set (parent takes precedence) + id = 'childValue'; + draggable = false; + hidden = false; + spellcheck = false; + tabindex = -1; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/main/main.html new file mode 100644 index 0000000000..9a76d73a6c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/main/main.html @@ -0,0 +1,16 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/main/main.js new file mode 100644 index 0000000000..5ca43c9ea7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/global-parent-overrides/x/main/main.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + value = { + id: 'parentProvided', + draggable: true, + spellcheck: true, + tabindex: 0, + hidden: true, + }; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/index.spec.js new file mode 100644 index 0000000000..60f88e709d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/index.spec.js @@ -0,0 +1,19 @@ +function getAllDivs(target) { + const childs = [...target.shadowRoot.querySelectorAll('x-child')]; + return childs.flatMap((child) => [...child.shadowRoot.querySelectorAll('div')]); +} +export default { + snapshot(target) { + const divs = getAllDivs(target); + return { divs }; + }, + test(target, snapshots, consoleCalls) { + const divs = getAllDivs(target); + expect(divs.length).toBe(snapshots.divs.length); + for (let i = 0; i < divs.length; i += 1) { + expect(divs[i]).toBe(snapshots.divs[i]); + } + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/child/child.html new file mode 100644 index 0000000000..32e1b71283 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/child/child.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/main/main.html new file mode 100644 index 0000000000..ae86b5ad0a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/main/main.html @@ -0,0 +1,28 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/main/main.js new file mode 100644 index 0000000000..7746c95e2c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/non-reflective/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + value = { + contenteditable: 'true', + }; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/index.spec.js new file mode 100644 index 0000000000..60f88e709d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/index.spec.js @@ -0,0 +1,19 @@ +function getAllDivs(target) { + const childs = [...target.shadowRoot.querySelectorAll('x-child')]; + return childs.flatMap((child) => [...child.shadowRoot.querySelectorAll('div')]); +} +export default { + snapshot(target) { + const divs = getAllDivs(target); + return { divs }; + }, + test(target, snapshots, consoleCalls) { + const divs = getAllDivs(target); + expect(divs.length).toBe(snapshots.divs.length); + for (let i = 0; i < divs.length; i += 1) { + expect(divs[i]).toBe(snapshots.divs[i]); + } + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/child/child.html new file mode 100644 index 0000000000..faffc84109 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/child/child.html @@ -0,0 +1,13 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/child/child.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/main/main.html new file mode 100644 index 0000000000..ae86b5ad0a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/main/main.html @@ -0,0 +1,28 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/main/main.js new file mode 100644 index 0000000000..99110bdf9f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/reflective/x/main/main.js @@ -0,0 +1,17 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + value = { + accesskey: 'tata', + arialabel: 'titi', + dir: 'auto', + draggable: false, + hidden: false, + id: 'tutu', + lang: 'jp', + role: 'scrollbar', + spellcheck: false, + tabindex: '0', + title: 'tete', + }; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/index.spec.js new file mode 100644 index 0000000000..d144f03346 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/index.spec.js @@ -0,0 +1,15 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('connectedCallback:true'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/x/main/main.html new file mode 100644 index 0000000000..d958c7031d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/x/main/main.js new file mode 100644 index 0000000000..536029b45c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/connected-callback/x/main/main.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + called = false; + connectedCallback() { + this.called = true; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/index.spec.js new file mode 100644 index 0000000000..49a9044c3f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/index.spec.js @@ -0,0 +1,28 @@ +let disconnectedCalled = false; + +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + showFoo: true, + disconnectedCb: () => { + disconnectedCalled = true; + }, + }, + snapshot(target) { + return { + xFoo: target.shadowRoot.querySelector('x-foo'), + }; + }, + test(target, snapshots) { + const xFoo = target.shadowRoot.querySelector('x-foo'); + expect(xFoo).not.toBe(null); + expect(xFoo).toBe(snapshots.xFoo); + + target.showFoo = false; + + return Promise.resolve().then(() => { + expect(disconnectedCalled).toBe(true); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/foo/foo.html b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/foo/foo.html new file mode 100644 index 0000000000..3323d4e315 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/foo/foo.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/foo/foo.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/foo/foo.js new file mode 100644 index 0000000000..5dda3d0d96 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/foo/foo.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class Foo extends LightningElement { + @api disconnectedCb; + + disconnectedCallback() { + this.disconnectedCb.call(null); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/main/main.html new file mode 100644 index 0000000000..4f110f35a5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/main/main.html @@ -0,0 +1,6 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/main/main.js new file mode 100644 index 0000000000..c539fe590a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/disconnected-callback/x/main/main.js @@ -0,0 +1,10 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api disconnectedCb; + @api showFoo; + + disconnectedCallback() { + this.disconnectedCb.call(null); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/index.spec.js new file mode 100644 index 0000000000..feae6ccffb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/index.spec.js @@ -0,0 +1,24 @@ +export default { + props: { + useTplA: true, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('template A'); + + target.useTplA = false; + + return Promise.resolve().then(() => { + expect(target.shadowRoot.querySelector('p').textContent).toBe('template B'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/a.html b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/a.html new file mode 100644 index 0000000000..1aa0588b62 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/a.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/b.html b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/b.html new file mode 100644 index 0000000000..012c7a045a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/b.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/main.js new file mode 100644 index 0000000000..357356aab6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/render-method/x/main/main.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; +import tplA from './a.html'; +import tplB from './b.html'; + +export default class Main extends LightningElement { + @api useTplA; + + render() { + return this.useTplA ? tplA : tplB; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/index.spec.js new file mode 100644 index 0000000000..4754206100 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/index.spec.js @@ -0,0 +1,19 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('renderedCallback:false'); + + return Promise.resolve().then(() => { + expect(p.textContent).toBe('renderedCallback:true'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/x/main/main.html new file mode 100644 index 0000000000..caeecb703f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/x/main/main.js new file mode 100644 index 0000000000..cd483b8092 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/component-lifecycle/rendered-callback/x/main/main.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + called = false; + renderedCallback() { + this.called = true; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/context/index.spec.js new file mode 100644 index 0000000000..ed71537fac --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/index.spec.js @@ -0,0 +1,101 @@ +export default { + // server is expected to generate the same console error as the client + expectedSSRConsoleCalls: { + error: [], + warn: [ + 'Attempted to connect to trusted context but received the following error', + 'Multiple contexts of the same variety were provided. Only the first context will be used.', + ], + }, + requiredFeatureFlags: ['ENABLE_EXPERIMENTAL_SIGNALS'], + snapshot(target) { + const grandparent = target.shadowRoot.querySelector('x-grandparent'); + const detachedChild = target.shadowRoot.querySelector('x-child'); + const firstParent = grandparent.shadowRoot.querySelectorAll('x-parent')[0]; + const secondParent = grandparent.shadowRoot.querySelectorAll('x-parent')[1]; + const childOfFirstParent = firstParent.shadowRoot.querySelector('x-child'); + const childOfSecondParent = secondParent.shadowRoot.querySelector('x-child'); + + return { + components: { + grandparent, + firstParent, + secondParent, + childOfFirstParent, + childOfSecondParent, + }, + detachedChild, + }; + }, + test(target, snapshot, consoleCalls) { + // Assert context is provided by the grandparent and consumed correctly by all children + assertCorrectContext(snapshot); + + // Assert context is shadowed when consumed in a chain + assertContextShadowed(snapshot); + + // Assert context is disconnected when components are removed + assertContextDisconnected(target, snapshot); + + // Expect an error as one context was generated twice. + // Expect an error as one context was malformed (did not define connectContext or disconnectContext methods). + // Expect server/client context output parity (no hydration warnings) + TestUtils.expectConsoleCalls(consoleCalls, { + error: [], + warn: [ + 'Attempted to connect to trusted context but received the following error', + 'Multiple contexts of the same variety were provided. Only the first context will be used.', + ], + }); + }, +}; + +function assertCorrectContext(snapshot) { + Object.values(snapshot.components).forEach((component) => { + expect(component.shadowRoot.querySelector('div').textContent) + .withContext(`${component.tagName} should have the correct context`) + .toBe('grandparent provided value, another grandparent provided value'); + + expect(component.context.connectProvidedComponent?.hostElement) + .withContext( + `The context of ${component.tagName} should have been connected with the correct component` + ) + .toBe(component); + }); + expect(snapshot.detachedChild.shadowRoot.querySelector('div').textContent).toBe(', '); +} + +function assertContextShadowed(snapshot) { + const grandparentContext = snapshot.components.grandparent.context; + const firstParentContext = snapshot.components.firstParent.context; + const childOfFirstParentContext = snapshot.components.childOfFirstParent.context; + + expect(childOfFirstParentContext.providedContextSignal) + .withContext( + `Child should have been provided with the parent context and not that of the grandparent (grandparent context was shadowed)` + ) + .toBe(firstParentContext); + + expect(firstParentContext.providedContextSignal) + .withContext(`Parent should have been provided with grandparent context`) + .toBe(grandparentContext); + + // For good measure + expect(grandparentContext) + .withContext(`Grandparent context should not be the same as the parent context`) + .not.toBe(firstParentContext); +} + +function assertContextDisconnected(target, snapshot) { + Object.values(snapshot.components).forEach( + (component) => + (component.disconnect = () => { + expect(component.context.disconnectProvidedComponent?.hostElement) + .withContext( + `The context of ${component.tagName} should have been disconnected with the correct component` + ) + .toBe(component); + }) + ); + target.showTree = false; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/base/base.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/base/base.js new file mode 100644 index 0000000000..6c2ca30bc5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/base/base.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; + +export default class Base extends LightningElement { + @api disconnect; + + disconnectedCallback() { + if (this.disconnect) { + this.disconnect(); + } + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/context/x/child/child.html new file mode 100644 index 0000000000..6db837b7e4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/child/child.js new file mode 100644 index 0000000000..7763fa7fdb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/child/child.js @@ -0,0 +1,9 @@ +import { api } from 'lwc'; +import Base from 'x/base'; +import { defineContext } from 'x/contextManager'; +import { parentContextFactory, anotherParentContextFactory } from 'x/parentContext'; + +export default class Child extends Base { + @api context = defineContext(parentContextFactory)(); + @api anotherContext = defineContext(anotherParentContextFactory)(); +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/contextManager/contextManager.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/contextManager/contextManager.js new file mode 100644 index 0000000000..d66e89e809 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/contextManager/contextManager.js @@ -0,0 +1,57 @@ +import { + setContextKeys, + setTrustedContextSet, + __dangerous_do_not_use_addTrustedContext, +} from 'lwc'; + +const connectContext = Symbol('connectContext'); +const disconnectContext = Symbol('disconnectContext'); +const trustedContext = new WeakSet(); + +setTrustedContextSet(trustedContext); +setContextKeys({ connectContext, disconnectContext }); + +class MockContextSignal { + connectProvidedComponent; + disconnectProvidedComponent; + providedContextSignal; + + constructor(initialValue, contextDefinition, fromContext) { + this.value = initialValue; + this.contextDefinition = contextDefinition; + this.fromContext = fromContext; + __dangerous_do_not_use_addTrustedContext(this); + } + [connectContext](runtimeAdapter) { + this.connectProvidedComponent = runtimeAdapter.component; + + runtimeAdapter.provideContext(this.contextDefinition, this); + + if (this.fromContext) { + runtimeAdapter.consumeContext(this.fromContext, (providedContextSignal) => { + this.providedContextSignal = providedContextSignal; + this.value = providedContextSignal.value; + }); + } + } + [disconnectContext](component) { + this.disconnectProvidedComponent = component; + } +} + +// This is a malformed context signal that does not implement the connectContext or disconnectContext methods +class MockMalformedContextSignal { + constructor() { + trustedContext.add(this); + } +} + +export const defineContext = (fromContext) => { + const contextDefinition = (initialValue) => + new MockContextSignal(initialValue, contextDefinition, fromContext); + return contextDefinition; +}; + +export const defineMalformedContext = () => { + return () => new MockMalformedContextSignal(); +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparent/grandparent.html b/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparent/grandparent.html new file mode 100644 index 0000000000..2c178bf46a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparent/grandparent.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparent/grandparent.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparent/grandparent.js new file mode 100644 index 0000000000..8bb4b172a5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparent/grandparent.js @@ -0,0 +1,8 @@ +import { api } from 'lwc'; +import Base from 'x/base'; +import { grandparentContextFactory, anotherGrandparentContextFactory } from 'x/grandparentContext'; + +export default class Grandparent extends Base { + @api context = grandparentContextFactory('grandparent provided value'); + @api anotherContext = anotherGrandparentContextFactory('another grandparent provided value'); +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparentContext/grandparentContext.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparentContext/grandparentContext.js new file mode 100644 index 0000000000..807ac8874a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/grandparentContext/grandparentContext.js @@ -0,0 +1,4 @@ +import { defineContext } from 'x/contextManager'; + +export const grandparentContextFactory = defineContext(); +export const anotherGrandparentContextFactory = defineContext(); diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/context/x/main/main.html new file mode 100644 index 0000000000..2241f1adbf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/main/main.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/main/main.js new file mode 100644 index 0000000000..0c63cfb133 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/main/main.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; +import { defineMalformedContext } from 'x/contextManager'; +export default class Root extends LightningElement { + @api showTree = false; + // Only test in CSR right now as SSR throws which prevents content from being rendered. There is additional fixtures ssr coverage for this case. + malformedContext = typeof window !== 'undefined' ? defineMalformedContext()() : undefined; + + connectedCallback() { + this.showTree = true; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/parent/parent.html b/packages/@lwc/integration-not-karma/test-hydration/context/x/parent/parent.html new file mode 100644 index 0000000000..a8e86f7a9b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/parent/parent.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/parent/parent.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/parent/parent.js new file mode 100644 index 0000000000..3e2b8689cf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/parent/parent.js @@ -0,0 +1,8 @@ +import { api } from 'lwc'; +import { parentContextFactory, anotherParentContextFactory } from 'x/parentContext'; +import Base from 'x/base'; + +export default class Parent extends Base { + @api context = parentContextFactory(); + @api anotherContext = anotherParentContextFactory(); +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/parentContext/parentContext.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/parentContext/parentContext.js new file mode 100644 index 0000000000..03a078239f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/parentContext/parentContext.js @@ -0,0 +1,5 @@ +import { defineContext } from 'x/contextManager'; +import { grandparentContextFactory, anotherGrandparentContextFactory } from 'x/grandparentContext'; + +export const parentContextFactory = defineContext(grandparentContextFactory); +export const anotherParentContextFactory = defineContext(anotherGrandparentContextFactory); diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.html b/packages/@lwc/integration-not-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.html new file mode 100644 index 0000000000..cb3b7ad81c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js b/packages/@lwc/integration-not-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js new file mode 100644 index 0000000000..513c0fb64a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js @@ -0,0 +1,11 @@ +import { grandparentContextFactory } from 'x/grandparentContext'; +import { LightningElement } from 'lwc'; + +export default class TooMuchContext extends LightningElement { + context = grandparentContextFactory('grandparent provided value'); + // Only test in CSR right now as it throws in SSR. There is additional fixtures ssr coverage for this case. + tooMuch = + typeof window !== 'undefined' + ? grandparentContextFactory('this world is not big enough for me') + : undefined; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/comments/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/comments/index.spec.js new file mode 100644 index 0000000000..9feeac3475 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/comments/index.spec.js @@ -0,0 +1,26 @@ +export default { + props: { + control: true, + }, + snapshot(target) { + const [firstComment, p] = target.shadowRoot.childNodes; + const [secondComment, text] = p.childNodes; + return { + firstComment, + p, + secondComment, + text, + }; + }, + test(target, snapshots) { + const [firstComment, p] = target.shadowRoot.childNodes; + const [secondComment, text] = p.childNodes; + + expect(firstComment).toBe(snapshots.firstComment); + expect(firstComment.nodeValue).toBe('first comment'); + expect(p).toBe(snapshots.p); + expect(secondComment).toBe(snapshots.secondComment); + expect(secondComment.nodeValue).toBe('comment inside element'); + expect(text).toBe(snapshots.text); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/comments/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/comments/x/main/main.html new file mode 100644 index 0000000000..aca22f191d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/comments/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/comments/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/comments/x/main/main.js new file mode 100644 index 0000000000..0eaadfe543 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/comments/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api control; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/index.spec.js new file mode 100644 index 0000000000..41ceea382e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + div: target.shadowRoot.querySelector('div'), + }; + }, + test(target, snapshots) { + const div = target.shadowRoot.querySelector('div'); + + expect(div).toBe(snapshots.div); + expect(div.innerHTML).toBe('

test

'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/x/main/main.html new file mode 100644 index 0000000000..afd9355489 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/x/main/main.js new file mode 100644 index 0000000000..ec1b237c50 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/dom-manual/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + renderedCallback() { + this.template.querySelector('div').innerHTML = '

test

'; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/index.spec.js new file mode 100644 index 0000000000..6bac7c837f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/index.spec.js @@ -0,0 +1,31 @@ +export default { + props: { + colors: ['red', 'yellow', 'blue'], + }, + snapshot(target) { + return { + ul: target.shadowRoot.querySelector('ul'), + colors: target.shadowRoot.querySelectorAll('li'), + }; + }, + test(target, snapshots) { + const ul = target.shadowRoot.querySelector('ul'); + let colors = ul.querySelectorAll('li'); + expect(ul).toBe(snapshots.ul); + expect(colors[0]).toBe(snapshots.colors[0]); + expect(colors[0].textContent).toBe('red'); + expect(colors[1]).toBe(snapshots.colors[1]); + expect(colors[1].textContent).toBe('yellow'); + expect(colors[2]).toBe(snapshots.colors[2]); + expect(colors[2].textContent).toBe('blue'); + + target.colors = ['orange', 'green', 'violet']; + + return Promise.resolve().then(() => { + colors = ul.querySelectorAll('li'); + expect(colors[0].textContent).toBe('orange'); + expect(colors[1].textContent).toBe('green'); + expect(colors[2].textContent).toBe('violet'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/x/main/main.html new file mode 100644 index 0000000000..f478978aa9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/x/main/main.js new file mode 100644 index 0000000000..88c9d7ed9d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/for-each/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api colors; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/index.spec.js new file mode 100644 index 0000000000..0a7930b796 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/index.spec.js @@ -0,0 +1,19 @@ +export default { + props: { + control: false, + }, + snapshot(target) { + return { + p: target.shadowRoot.querySelector('p'), + }; + }, + test(target, snapshots) { + expect(target.shadowRoot.querySelector('p')).toBe(snapshots.p); + + target.control = true; + + return Promise.resolve().then(() => { + expect(target.shadowRoot.querySelector('p')).toBeNull(); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/x/main/main.html new file mode 100644 index 0000000000..6cdc0f58a6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/x/main/main.js new file mode 100644 index 0000000000..0eaadfe543 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/if-false/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api control; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/index.spec.js new file mode 100644 index 0000000000..196e52d647 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/index.spec.js @@ -0,0 +1,19 @@ +export default { + props: { + control: true, + }, + snapshot(target) { + return { + p: target.shadowRoot.querySelector('p'), + }; + }, + test(target, snapshots) { + expect(target.shadowRoot.querySelector('p')).toBe(snapshots.p); + + target.control = false; + + return Promise.resolve().then(() => { + expect(target.shadowRoot.querySelector('p')).toBeNull(); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/x/main/main.html new file mode 100644 index 0000000000..c5d80d15d4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/x/main/main.js new file mode 100644 index 0000000000..0eaadfe543 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/if-true/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api control; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/index.spec.js new file mode 100644 index 0000000000..6bac7c837f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/index.spec.js @@ -0,0 +1,31 @@ +export default { + props: { + colors: ['red', 'yellow', 'blue'], + }, + snapshot(target) { + return { + ul: target.shadowRoot.querySelector('ul'), + colors: target.shadowRoot.querySelectorAll('li'), + }; + }, + test(target, snapshots) { + const ul = target.shadowRoot.querySelector('ul'); + let colors = ul.querySelectorAll('li'); + expect(ul).toBe(snapshots.ul); + expect(colors[0]).toBe(snapshots.colors[0]); + expect(colors[0].textContent).toBe('red'); + expect(colors[1]).toBe(snapshots.colors[1]); + expect(colors[1].textContent).toBe('yellow'); + expect(colors[2]).toBe(snapshots.colors[2]); + expect(colors[2].textContent).toBe('blue'); + + target.colors = ['orange', 'green', 'violet']; + + return Promise.resolve().then(() => { + colors = ul.querySelectorAll('li'); + expect(colors[0].textContent).toBe('orange'); + expect(colors[1].textContent).toBe('green'); + expect(colors[2].textContent).toBe('violet'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/x/main/main.html new file mode 100644 index 0000000000..8e5140f566 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/x/main/main.js new file mode 100644 index 0000000000..88c9d7ed9d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/iterator/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api colors; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/index.spec.js new file mode 100644 index 0000000000..b6a49ef4ed --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + label: 'dynamic', + }, + snapshot(target) { + const cmp = target.shadowRoot.querySelector('x-child'); + const p = cmp.shadowRoot.querySelector('p'); + + return { + cmp, + p, + }; + }, + test(target, snapshots) { + const cmp = target.shadowRoot.querySelector('x-child'); + const p = cmp.shadowRoot.querySelector('p'); + + expect(cmp).toBe(snapshots.cmp); + expect(p).toBe(snapshots.p); + expect(p.textContent).toBe('dynamic'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/child/child.html new file mode 100644 index 0000000000..260d58602d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/child/child.js new file mode 100644 index 0000000000..e250b53c38 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + @api label; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/main/main.html new file mode 100644 index 0000000000..575bfc8555 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/main/main.js new file mode 100644 index 0000000000..d64d3311e8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-dynamic/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; +import Child from 'x/child'; + +export default class Main extends LightningElement { + @api label; + Ctor = Child; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/index.spec.js new file mode 100644 index 0000000000..3727317561 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + content: '

test-content

', + }, + snapshot(target) { + const div = target.shadowRoot.querySelector('div'); + const p = div.querySelector('p'); + return { + div, + p, + text: p.textContent, + }; + }, + test(target, snapshot) { + const div = target.shadowRoot.querySelector('div'); + const p = div.querySelector('p'); + + expect(div).toBe(snapshot.div); + expect(p).toBe(snapshot.p); + expect(p.textContent).toBe(snapshot.text); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/x/main/main.html new file mode 100644 index 0000000000..66a8e2c720 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/x/main/main.js new file mode 100644 index 0000000000..8066dd4ab7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-inner-html/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api content; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/index.spec.js new file mode 100644 index 0000000000..b6a49ef4ed --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + label: 'dynamic', + }, + snapshot(target) { + const cmp = target.shadowRoot.querySelector('x-child'); + const p = cmp.shadowRoot.querySelector('p'); + + return { + cmp, + p, + }; + }, + test(target, snapshots) { + const cmp = target.shadowRoot.querySelector('x-child'); + const p = cmp.shadowRoot.querySelector('p'); + + expect(cmp).toBe(snapshots.cmp); + expect(p).toBe(snapshots.p); + expect(p.textContent).toBe('dynamic'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/child/child.html new file mode 100644 index 0000000000..260d58602d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/child/child.js new file mode 100644 index 0000000000..e250b53c38 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + @api label; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/main/main.html new file mode 100644 index 0000000000..575bfc8555 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/main/main.js new file mode 100644 index 0000000000..d64d3311e8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-is/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; +import Child from 'x/child'; + +export default class Main extends LightningElement { + @api label; + Ctor = Child; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/index.spec.js new file mode 100644 index 0000000000..d9cb0c0ad8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/index.spec.js @@ -0,0 +1,20 @@ +export default { + props: {}, + snapshot(target) { + const div = target.shadowRoot.querySelector('div'); + + return { + div, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.div).toBe(snapshots.div); + + // verify handler + snapshotAfterHydration.div.click(); + expect(target.timesClickedHandlerIsExecuted).toBe(1); + snapshotAfterHydration.div.dispatchEvent(new CustomEvent('foo')); + expect(target.timesFooHandlerIsExecuted).toBe(1); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/x/main/main.html new file mode 100644 index 0000000000..cabec61e8b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/x/main/main.js new file mode 100644 index 0000000000..4464d2fd7b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/directives/lwc-on/x/main/main.js @@ -0,0 +1,25 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + _clickedHandlerCounter = 0; + _fooHandlerCounter = 0; + + @api + get timesClickedHandlerIsExecuted() { + return this._clickedHandlerCounter; + } + + @api + get timesFooHandlerIsExecuted() { + return this._fooHandlerCounter; + } + + eventListeners = { + click: function () { + this._clickedHandlerCounter++; + }, + foo: function () { + this._fooHandlerCounter++; + }, + }; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/index.spec.js new file mode 100644 index 0000000000..251c761b6e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/index.spec.js @@ -0,0 +1,13 @@ +export default { + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + hydrateComponent(target, Component, {}); + + hydrateComponent(target, Component, {}); + + const consoleCalls = consoleSpy.calls; + + TestUtils.expectConsoleCalls(consoleCalls, { + warn: ['"hydrateComponent" expects an element that is not hydrated.'], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/x/main/main.html new file mode 100644 index 0000000000..c43fc008ba --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/index.spec.js new file mode 100644 index 0000000000..775cda244f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/index.spec.js @@ -0,0 +1,18 @@ +export default { + props: {}, + snapshot(target) { + const button = target.shadowRoot.querySelector('button'); + + return { + button, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.button).toBe(snapshots.button); + + // verify handler + snapshotAfterHydration.button.click(); + expect(target.timesHandlerIsExecuted).toBe(1); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/x/main/main.html new file mode 100644 index 0000000000..365cb4c1f1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/x/main/main.js new file mode 100644 index 0000000000..d283549853 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/events/dynamic/x/main/main.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + dynamic = 'I am dynamic'; + _executedHandlerCounter = 0; + + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/events/static/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/events/static/index.spec.js new file mode 100644 index 0000000000..775cda244f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/events/static/index.spec.js @@ -0,0 +1,18 @@ +export default { + props: {}, + snapshot(target) { + const button = target.shadowRoot.querySelector('button'); + + return { + button, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.button).toBe(snapshots.button); + + // verify handler + snapshotAfterHydration.button.click(); + expect(target.timesHandlerIsExecuted).toBe(1); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/events/static/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/events/static/x/main/main.html new file mode 100644 index 0000000000..b4bab7ae61 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/events/static/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/events/static/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/events/static/x/main/main.js new file mode 100644 index 0000000000..f10a039ba8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/events/static/x/main/main.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + _executedHandlerCounter = 0; + + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/index.spec.js new file mode 100644 index 0000000000..4d584c357a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/index.spec.js @@ -0,0 +1,16 @@ +export default { + props: {}, + advancedTest(target, { consoleSpy }) { + const ids = Object.entries(TestUtils.extractDataIds(target)).filter( + ([id]) => !id.endsWith('.shadowRoot') + ); + for (const [id, node] of ids) { + expect(node.childNodes.length).toBe(1); + expect(node.firstChild.nodeType).toBe(Node.TEXT_NODE); + const expected = id.startsWith('lwc-inner-html-') ? 'injected' : 'original'; + expect(node.firstChild.nodeValue).toBe(expected); + } + expect(consoleSpy.calls.warn).toHaveSize(0); + expect(consoleSpy.calls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/component/component.html b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/component/component.html new file mode 100644 index 0000000000..6806310398 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/component/component.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/component/component.js b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/component/component.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/main/main.html new file mode 100644 index 0000000000..3e9615e892 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/main/main.html @@ -0,0 +1,20 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/main/main.js new file mode 100644 index 0000000000..dc4b54ded2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/x/main/main.js @@ -0,0 +1,6 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + computed = 'injected'; + spread = { innerHTML: 'wheeeeeeeeeeeeeeeeeeeeeeeeeee' }; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/index.spec.js new file mode 100644 index 0000000000..dcb24ab10b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/index.spec.js @@ -0,0 +1,334 @@ +// `` and `` have a peculiar attr/prop relationship, so the engine +// has historically treated them as props rather than attributes: +// https://github.com/salesforce/lwc/blob/b584d39/packages/%40lwc/template-compiler/src/parser/attribute.ts#L217-L221 +// For example, an element might be rendered as `` but `input.checked` could +// still return true. `value` behaves similarly. `value` and `checked` behave surprisingly +// because the attributes actually represent the "default" value rather than the current one: +// - https://jakearchibald.com/2024/attributes-vs-properties/#value-on-input-fields +// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#checked +// Here we check both the "default" and "runtime" variants of `checked`/`value`. Note that +// `defaultChecked`/`defaultValue` correspond to the `checked`/`value` attributes. +function getRelevantInputProps(input) { + const { defaultChecked, defaultValue, disabled, checked, type, value } = input; + return { + checked, + defaultChecked, + defaultValue, + disabled, + type, + value, + }; +} + +export default { + snapshot(target) { + const inputs = target.shadowRoot.querySelectorAll('input'); + return { + inputs, + }; + }, + test(target, snapshots, consoleCalls) { + const inputs = target.shadowRoot.querySelectorAll('input'); + + expect(inputs.length).toBe(snapshots.inputs.length); + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + expect(input).toBe(snapshots.inputs[i]); + } + + // prop values are as expected + expect([...inputs].map(getRelevantInputProps)).toEqual([ + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'undefined', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'false', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'true', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '0', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '0', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'NaN', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'Infinity', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '-Infinity', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'foo,bar', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '[object Object]', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + ]); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/x/main/main.html new file mode 100644 index 0000000000..ff13db4b2d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/x/main/main.html @@ -0,0 +1,40 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/x/main/main.js new file mode 100644 index 0000000000..26486468a3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/input/dynamic/x/main/main.js @@ -0,0 +1,16 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + isUndefined = undefined; + isNull = null; + isFalse = false; + isTrue = true; + isZero = 0; + isNegZero = -0; + isNaN = NaN; + isInfinity = Infinity; + isNegInfinity = -Infinity; + isEmptyString = ''; + isArray = ['foo', 'bar']; + isObject = { foo: 'bar', baz: 'quux' }; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/input/static/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/input/static/index.spec.js new file mode 100644 index 0000000000..fb233a993e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/input/static/index.spec.js @@ -0,0 +1,209 @@ +// `` and `` have a peculiar attr/prop relationship, so the engine +// has historically treated them as props rather than attributes: +// https://github.com/salesforce/lwc/blob/b584d39/packages/%40lwc/template-compiler/src/parser/attribute.ts#L217-L221 +// For example, an element might be rendered as `` but `input.checked` could +// still return true. `value` behaves similarly. `value` and `checked` behave surprisingly +// because the attributes actually represent the "default" value rather than the current one: +// - https://jakearchibald.com/2024/attributes-vs-properties/#value-on-input-fields +// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#checked +// Here we check both the "default" and "runtime" variants of `checked`/`value`. Note that +// `defaultChecked`/`defaultValue` correspond to the `checked`/`value` attributes. +function getRelevantInputProps(input) { + const { defaultChecked, defaultValue, disabled, checked, type, value } = input; + return { + checked, + defaultChecked, + defaultValue, + disabled, + type, + value, + }; +} + +export default { + snapshot(target) { + const inputs = target.shadowRoot.querySelectorAll('input'); + return { + inputs, + }; + }, + test(target, snapshots, consoleCalls) { + const inputs = target.shadowRoot.querySelectorAll('input'); + + expect(inputs.length).toBe(snapshots.inputs.length); + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + expect(input).toBe(snapshots.inputs[i]); + + // "default" checked/value are not set by either SSR or runtime + expect(input.defaultValue).toBe(''); + expect(input.defaultChecked).toBe(false); + } + + expect([...inputs].map(getRelevantInputProps)).toEqual([ + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: true, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'checkbox', + value: 'on', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'true', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'value', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'false', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'true', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'FALSE', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'TRUE', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: false, + type: 'text', + value: 'yolo', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + { + checked: false, + defaultChecked: false, + defaultValue: '', + disabled: true, + type: 'text', + value: '', + }, + ]); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/input/static/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/input/static/x/main/main.html new file mode 100644 index 0000000000..65f21a30cd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/input/static/x/main/main.html @@ -0,0 +1,24 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/input/static/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/input/static/x/main/main.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/input/static/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/index.spec.js new file mode 100644 index 0000000000..1cdf5a2f25 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/index.spec.js @@ -0,0 +1,15 @@ +export default { + snapshot(target) { + const p = target.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('connectedCallback:true'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/x/main/main.html new file mode 100644 index 0000000000..4c60746d95 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/x/main/main.js new file mode 100644 index 0000000000..1a46e269c8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/connected-callback/x/main/main.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + called = false; + connectedCallback() { + this.called = true; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/index.spec.js new file mode 100644 index 0000000000..786b3e7f9f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/index.spec.js @@ -0,0 +1,28 @@ +let disconnectedCalled = false; + +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + showFoo: true, + disconnectedCb: () => { + disconnectedCalled = true; + }, + }, + snapshot(target) { + return { + xFoo: target.querySelector('x-foo'), + }; + }, + test(target, snapshots) { + const xFoo = target.querySelector('x-foo'); + expect(xFoo).not.toBe(null); + expect(xFoo).toBe(snapshots.xFoo); + + target.showFoo = false; + + return Promise.resolve().then(() => { + expect(disconnectedCalled).toBe(true); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/foo/foo.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/foo/foo.html new file mode 100644 index 0000000000..73ac2428e3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/foo/foo.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/foo/foo.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/foo/foo.js new file mode 100644 index 0000000000..783fcb21d5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/foo/foo.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; + +export default class Foo extends LightningElement { + static renderMode = 'light'; + + @api disconnectedCb; + + disconnectedCallback() { + this.disconnectedCb.call(null); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/main/main.html new file mode 100644 index 0000000000..0796af64d2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/main/main.html @@ -0,0 +1,6 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/main/main.js new file mode 100644 index 0000000000..0a890e26e1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/disconnected-callback/x/main/main.js @@ -0,0 +1,12 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api disconnectedCb; + @api showFoo; + + disconnectedCallback() { + this.disconnectedCb.call(null); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/index.spec.js new file mode 100644 index 0000000000..599a9f8991 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/index.spec.js @@ -0,0 +1,24 @@ +export default { + props: { + useTplA: true, + }, + snapshot(target) { + const p = target.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('template A'); + + target.useTplA = false; + + return Promise.resolve().then(() => { + expect(target.querySelector('p').textContent).toBe('template B'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/a.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/a.html new file mode 100644 index 0000000000..96f5ba7d49 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/a.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/b.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/b.html new file mode 100644 index 0000000000..aa9c8b69ca --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/b.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/main.js new file mode 100644 index 0000000000..26b629428c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/render-method/x/main/main.js @@ -0,0 +1,13 @@ +import { LightningElement, api } from 'lwc'; +import tplA from './a.html'; +import tplB from './b.html'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api useTplA; + + render() { + return this.useTplA ? tplA : tplB; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/index.spec.js new file mode 100644 index 0000000000..5ecd60410c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/index.spec.js @@ -0,0 +1,19 @@ +export default { + snapshot(target) { + const p = target.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('renderedCallback:false'); + + return Promise.resolve().then(() => { + expect(p.textContent).toBe('renderedCallback:true'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/x/main/main.html new file mode 100644 index 0000000000..b53f4312f2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/x/main/main.js new file mode 100644 index 0000000000..059f546fa9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/component-lifecycle/rendered-callback/x/main/main.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + called = false; + renderedCallback() { + this.called = true; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/index.spec.js new file mode 100644 index 0000000000..9c3fb4e8b8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/index.spec.js @@ -0,0 +1,26 @@ +export default { + props: { + control: true, + }, + snapshot(target) { + const [firstComment, p] = target.childNodes; + const [secondComment, text] = p.childNodes; + return { + firstComment, + p, + secondComment, + text, + }; + }, + test(target, snapshots) { + const [firstComment, p] = target.childNodes; + const [secondComment, text] = p.childNodes; + + expect(firstComment).toBe(snapshots.firstComment); + expect(firstComment.nodeValue).toBe('first comment'); + expect(p).toBe(snapshots.p); + expect(secondComment).toBe(snapshots.secondComment); + expect(secondComment.nodeValue).toBe('comment inside element'); + expect(text).toBe(snapshots.text); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/x/main/main.html new file mode 100644 index 0000000000..16df91450f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/x/main/main.js new file mode 100644 index 0000000000..86183b5f3e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/comments/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api control; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/index.spec.js new file mode 100644 index 0000000000..d9d4602085 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/index.spec.js @@ -0,0 +1,31 @@ +export default { + props: { + colors: ['red', 'yellow', 'blue'], + }, + snapshot(target) { + return { + ul: target.querySelector('ul'), + colors: target.querySelectorAll('li'), + }; + }, + test(target, snapshots) { + const ul = target.querySelector('ul'); + let colors = ul.querySelectorAll('li'); + expect(ul).toBe(snapshots.ul); + expect(colors[0]).toBe(snapshots.colors[0]); + expect(colors[0].textContent).toBe('red'); + expect(colors[1]).toBe(snapshots.colors[1]); + expect(colors[1].textContent).toBe('yellow'); + expect(colors[2]).toBe(snapshots.colors[2]); + expect(colors[2].textContent).toBe('blue'); + + target.colors = ['orange', 'green', 'violet']; + + return Promise.resolve().then(() => { + colors = ul.querySelectorAll('li'); + expect(colors[0].textContent).toBe('orange'); + expect(colors[1].textContent).toBe('green'); + expect(colors[2].textContent).toBe('violet'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/x/main/main.html new file mode 100644 index 0000000000..29f28a4539 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/x/main/main.js new file mode 100644 index 0000000000..afb3f7c9aa --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/for-each/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api colors; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/index.spec.js new file mode 100644 index 0000000000..bf7eeb7bcf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/index.spec.js @@ -0,0 +1,19 @@ +export default { + props: { + control: false, + }, + snapshot(target) { + return { + p: target.querySelector('p'), + }; + }, + test(target, snapshots) { + expect(target.querySelector('p')).toBe(snapshots.p); + + target.control = true; + + return Promise.resolve().then(() => { + expect(target.querySelector('p')).toBeNull(); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/x/main/main.html new file mode 100644 index 0000000000..4a4c5b1ce0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/x/main/main.js new file mode 100644 index 0000000000..86183b5f3e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-false/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api control; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/index.spec.js new file mode 100644 index 0000000000..33643e57be --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/index.spec.js @@ -0,0 +1,19 @@ +export default { + props: { + control: true, + }, + snapshot(target) { + return { + p: target.querySelector('p'), + }; + }, + test(target, snapshots) { + expect(target.querySelector('p')).toBe(snapshots.p); + + target.control = false; + + return Promise.resolve().then(() => { + expect(target.querySelector('p')).toBeNull(); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/x/main/main.html new file mode 100644 index 0000000000..2d51e71943 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/x/main/main.js new file mode 100644 index 0000000000..86183b5f3e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/if-true/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api control; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/index.spec.js new file mode 100644 index 0000000000..d9d4602085 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/index.spec.js @@ -0,0 +1,31 @@ +export default { + props: { + colors: ['red', 'yellow', 'blue'], + }, + snapshot(target) { + return { + ul: target.querySelector('ul'), + colors: target.querySelectorAll('li'), + }; + }, + test(target, snapshots) { + const ul = target.querySelector('ul'); + let colors = ul.querySelectorAll('li'); + expect(ul).toBe(snapshots.ul); + expect(colors[0]).toBe(snapshots.colors[0]); + expect(colors[0].textContent).toBe('red'); + expect(colors[1]).toBe(snapshots.colors[1]); + expect(colors[1].textContent).toBe('yellow'); + expect(colors[2]).toBe(snapshots.colors[2]); + expect(colors[2].textContent).toBe('blue'); + + target.colors = ['orange', 'green', 'violet']; + + return Promise.resolve().then(() => { + colors = ul.querySelectorAll('li'); + expect(colors[0].textContent).toBe('orange'); + expect(colors[1].textContent).toBe('green'); + expect(colors[2].textContent).toBe('violet'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/x/main/main.html new file mode 100644 index 0000000000..9fd57c2a31 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/x/main/main.js new file mode 100644 index 0000000000..afb3f7c9aa --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/iterator/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api colors; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/index.spec.js new file mode 100644 index 0000000000..77b5c3d75c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + label: 'dynamic', + }, + snapshot(target) { + const cmp = target.querySelector('x-child'); + const p = cmp.querySelector('p'); + + return { + cmp, + p, + }; + }, + test(target, snapshots) { + const cmp = target.querySelector('x-child'); + const p = cmp.querySelector('p'); + + expect(cmp).toBe(snapshots.cmp); + expect(p).toBe(snapshots.p); + expect(p.textContent).toBe('dynamic'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/child/child.html new file mode 100644 index 0000000000..a75c46f6e5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/child/child.js new file mode 100644 index 0000000000..ef40534ce2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/child/child.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + static renderMode = 'light'; + + @api label; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/main/main.html new file mode 100644 index 0000000000..ce8c44c26e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/main/main.js new file mode 100644 index 0000000000..722fc79723 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-dynamic/x/main/main.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; +import Child from 'x/child'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api label; + Ctor = Child; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/index.spec.js new file mode 100644 index 0000000000..8b29eddea5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/index.spec.js @@ -0,0 +1,25 @@ +export default { + props: { + content: '

test-content

', + }, + snapshot(target) { + const div = target.querySelector('div'); + const p = div.querySelector('p'); + return { + div, + p, + text: p.textContent, + }; + }, + test(target, snapshot, consoleCalls) { + const div = target.querySelector('div'); + const p = div.querySelector('p'); + + expect(div).toBe(snapshot.div); + expect(p).toBe(snapshot.p); + expect(p.textContent).toBe(snapshot.text); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/x/main/main.html new file mode 100644 index 0000000000..559313c99b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/x/main/main.js new file mode 100644 index 0000000000..df07c2bf3e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-inner-html/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api content; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/index.spec.js new file mode 100644 index 0000000000..77b5c3d75c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + label: 'dynamic', + }, + snapshot(target) { + const cmp = target.querySelector('x-child'); + const p = cmp.querySelector('p'); + + return { + cmp, + p, + }; + }, + test(target, snapshots) { + const cmp = target.querySelector('x-child'); + const p = cmp.querySelector('p'); + + expect(cmp).toBe(snapshots.cmp); + expect(p).toBe(snapshots.p); + expect(p.textContent).toBe('dynamic'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/child/child.html new file mode 100644 index 0000000000..a75c46f6e5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/child/child.js new file mode 100644 index 0000000000..ef40534ce2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/child/child.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + static renderMode = 'light'; + + @api label; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/main/main.html new file mode 100644 index 0000000000..ce8c44c26e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/main/main.js new file mode 100644 index 0000000000..722fc79723 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/directives/lwc-is/x/main/main.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; +import Child from 'x/child'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + @api label; + Ctor = Child; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/index.spec.js new file mode 100644 index 0000000000..772d602562 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/index.spec.js @@ -0,0 +1,23 @@ +export default { + snapshot(target) { + const shadowChild = target.querySelector('[data-id="x-shadow-child"]'); + + return { + lightParent: target, + parentText: target.querySelector('[data-id="parent-text"]'), + shadowChild, + childText: shadowChild.shadowRoot.querySelector('[data-id="child-text"]'), + }; + }, + test(target, snapshots) { + const hydratedSnapshot = this.snapshot(target); + + expect(hydratedSnapshot.lightParent).toBe(snapshots.lightParent); + expect(hydratedSnapshot.parentText).toBe(snapshots.parentText); + expect(hydratedSnapshot.shadowChild).toBe(snapshots.shadowChild); + expect(hydratedSnapshot.childText).toBe(snapshots.childText); + + expect(hydratedSnapshot.parentText.textContent).toEqual('inside parent'); + expect(hydratedSnapshot.childText.textContent).toEqual('inside child'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/main/main.html new file mode 100644 index 0000000000..fd151ea4de --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/main/main.js new file mode 100644 index 0000000000..0f60e8b223 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/shadowChild/shadowChild.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/shadowChild/shadowChild.html new file mode 100644 index 0000000000..f0475ce78c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/shadowChild/shadowChild.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/shadowChild/shadowChild.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/shadowChild/shadowChild.js new file mode 100644 index 0000000000..405b116075 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/light-parent-shadow-child/x/shadowChild/shadowChild.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class ShadowChild extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/index.spec.js new file mode 100644 index 0000000000..6d91b566f1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/index.spec.js @@ -0,0 +1,14 @@ +export default { + test(target) { + expect(getComputedStyle(target).backgroundColor).toEqual('rgb(0, 0, 255)'); + expect(getComputedStyle(target.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(255, 0, 0)' + ); + + return Promise.resolve().then(() => { + expect( + getComputedStyle(target.shadowRoot.querySelector('x-light-child div')).color + ).not.toEqual('rgb(255, 0, 0)'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/lightChild/lightChild.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/lightChild/lightChild.html new file mode 100644 index 0000000000..4042451180 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/lightChild/lightChild.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/lightChild/lightChild.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/lightChild/lightChild.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/lightChild/lightChild.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.html new file mode 100644 index 0000000000..eb975063f7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.scoped.css new file mode 100644 index 0000000000..1528e312bd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/can-scope-shadow-dom-styles/x/main/main.scoped.css @@ -0,0 +1,7 @@ +:host { + background: blue; +} + +div { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/index.spec.js new file mode 100644 index 0000000000..3786ef65f3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/index.spec.js @@ -0,0 +1,24 @@ +export default { + snapshot(target) { + return { + basicElement: target.shadowRoot.querySelector('x-basic'), + otherElement: target.shadowRoot.querySelector('x-other'), + }; + }, + test(target, snapshots) { + const { basicElement, otherElement } = this.snapshot(target); + expect(basicElement).toBe(snapshots.basicElement); + expect(otherElement).toBe(snapshots.otherElement); + + const basicHostComputed = getComputedStyle(basicElement); + const basicComputed = getComputedStyle(basicElement.querySelector('div')); + const otherComputed = getComputedStyle(otherElement.querySelector('div')); + expect(basicHostComputed.backgroundColor).toEqual('rgb(255, 0, 0)'); + expect(basicComputed.color).toEqual('rgb(0, 128, 0)'); + expect(basicComputed.marginLeft).toEqual('10px'); + expect(basicComputed.marginRight).toEqual('5px'); + expect(otherComputed.color).toEqual('rgb(0, 0, 0)'); + expect(otherComputed.marginLeft).toEqual('10px'); + expect(otherComputed.marginRight).toEqual('5px'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.css new file mode 100644 index 0000000000..106d26b08b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.css @@ -0,0 +1,3 @@ +div { + margin-left: 10px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.html new file mode 100644 index 0000000000..21f7dcb6bf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.js new file mode 100644 index 0000000000..33c4a65565 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Basic extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.scoped.css new file mode 100644 index 0000000000..d81ae5ec22 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/basic/basic.scoped.css @@ -0,0 +1,7 @@ +:host { + background-color: red; +} + +div { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/main/main.html new file mode 100644 index 0000000000..f9c22146e1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.css new file mode 100644 index 0000000000..146b14a9ce --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.css @@ -0,0 +1,3 @@ +div { + margin-right: 5px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.html new file mode 100644 index 0000000000..21f7dcb6bf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.js new file mode 100644 index 0000000000..d7e9df134e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/css-allow-unscoped-css-leak-out/x/other/other.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Other extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/index.spec.js new file mode 100644 index 0000000000..84218066a9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/index.spec.js @@ -0,0 +1,9 @@ +export default { + test(target, snapshot, consoleCalls) { + // W-19087941: Expect no errors or warnings, hydration or otherwise + TestUtils.expectConsoleCalls(consoleCalls, { + error: [], + warn: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.html new file mode 100644 index 0000000000..6f06182f35 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.scoped.css new file mode 100644 index 0000000000..72e32daf4d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/child/child.scoped.css @@ -0,0 +1,3 @@ +.blue { + background-color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.css new file mode 100644 index 0000000000..edb1d0fb9d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.css @@ -0,0 +1,3 @@ +.main { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.html new file mode 100644 index 0000000000..9b2366dfc3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/index.spec.js new file mode 100644 index 0000000000..e7a776e357 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + parent: target, + child: target.querySelector('x-child'), + }; + }, + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + const { parent, child } = this.snapshot(target); + hydrateComponent(target, Component, {}); + const consoleCalls = consoleSpy.calls; + // Validate there is no class attribute mismatch + expect(consoleCalls.error).toHaveSize(0); + // Validate there is no hydration mismatch + expect(parent).toBe(target); + expect(child).toBe(target.querySelector('x-child')); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.html new file mode 100644 index 0000000000..7840cce9fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.scoped.css new file mode 100644 index 0000000000..d0bcad1033 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/child/child.scoped.css @@ -0,0 +1,3 @@ +:host { + padding: 8px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.css new file mode 100644 index 0000000000..edb1d0fb9d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.css @@ -0,0 +1,3 @@ +.main { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.html new file mode 100644 index 0000000000..2d69fe28ac --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/child-scoped-styles/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/index.spec.js new file mode 100644 index 0000000000..e7a776e357 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + parent: target, + child: target.querySelector('x-child'), + }; + }, + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + const { parent, child } = this.snapshot(target); + hydrateComponent(target, Component, {}); + const consoleCalls = consoleSpy.calls; + // Validate there is no class attribute mismatch + expect(consoleCalls.error).toHaveSize(0); + // Validate there is no hydration mismatch + expect(parent).toBe(target); + expect(child).toBe(target.querySelector('x-child')); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.css new file mode 100644 index 0000000000..d0bcad1033 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.css @@ -0,0 +1,3 @@ +:host { + padding: 8px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.html new file mode 100644 index 0000000000..7840cce9fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.html new file mode 100644 index 0000000000..2d69fe28ac --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.scoped.css new file mode 100644 index 0000000000..158f6463c1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/parent-scoped-styles/x/main/main.scoped.css @@ -0,0 +1,3 @@ +.main { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/index.spec.js new file mode 100644 index 0000000000..e7a776e357 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + parent: target, + child: target.querySelector('x-child'), + }; + }, + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + const { parent, child } = this.snapshot(target); + hydrateComponent(target, Component, {}); + const consoleCalls = consoleSpy.calls; + // Validate there is no class attribute mismatch + expect(consoleCalls.error).toHaveSize(0); + // Validate there is no hydration mismatch + expect(parent).toBe(target); + expect(child).toBe(target.querySelector('x-child')); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.css new file mode 100644 index 0000000000..d0bcad1033 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.css @@ -0,0 +1,3 @@ +:host { + padding: 8px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.html new file mode 100644 index 0000000000..7840cce9fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.css new file mode 100644 index 0000000000..5a5039b6e6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.css @@ -0,0 +1,3 @@ +:host { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.html new file mode 100644 index 0000000000..586e14e39f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/nested-scoped-styles/without-scoped-styles/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/index.spec.js new file mode 100644 index 0000000000..7dedd68aec --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/index.spec.js @@ -0,0 +1,31 @@ +export default { + test(target) { + const rafPromise = () => new Promise((resolve) => requestAnimationFrame(() => resolve())); + + // A (no styles) -> B (styles) -> C (no styles) -> D (styles) + expect(getComputedStyle(target).marginLeft).toEqual('0px'); + expect(getComputedStyle(target.querySelector('div')).color).toEqual('rgb(0, 0, 0)'); + target.next(); + return rafPromise() + .then(() => { + expect(getComputedStyle(target).marginLeft).toEqual('20px'); + expect(getComputedStyle(target.querySelector('div')).color).toEqual( + 'rgb(255, 0, 0)' + ); + target.next(); + return rafPromise(); + }) + .then(() => { + expect(getComputedStyle(target).marginLeft).toEqual('0px'); + expect(getComputedStyle(target.querySelector('div')).color).toEqual('rgb(0, 0, 0)'); + target.next(); + return rafPromise(); + }) + .then(() => { + expect(getComputedStyle(target).marginLeft).toEqual('30px'); + expect(getComputedStyle(target.querySelector('div')).color).toEqual( + 'rgb(0, 0, 255)' + ); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/a.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/a.html new file mode 100644 index 0000000000..4a39102bbe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/a.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/b.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/b.html new file mode 100644 index 0000000000..71ebd6813c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/b.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/b.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/b.scoped.css new file mode 100644 index 0000000000..39b50ac938 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/b.scoped.css @@ -0,0 +1,7 @@ +:host { + margin-left: 20px; +} + +div { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/c.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/c.html new file mode 100644 index 0000000000..e569b813fc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/c.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/d.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/d.html new file mode 100644 index 0000000000..be6170af9f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/d.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/d.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/d.scoped.css new file mode 100644 index 0000000000..d8c0961b2c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/d.scoped.css @@ -0,0 +1,7 @@ +:host { + margin-left: 30px; +} + +div { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/main.js new file mode 100644 index 0000000000..8ccdc8c1c7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/x/main/main.js @@ -0,0 +1,21 @@ +import { LightningElement, api } from 'lwc'; +import A from './a.html'; +import B from './b.html'; +import C from './c.html'; +import D from './d.html'; + +const templates = [A, B, C, D]; + +export default class Main extends LightningElement { + static renderMode = 'light'; + current = 0; + + @api + next() { + this.current++; + } + + render() { + return templates[this.current]; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/index.spec.js new file mode 100644 index 0000000000..7662a615c4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/index.spec.js @@ -0,0 +1,8 @@ +export default { + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + hydrateComponent(target, Component, {}); + const consoleCalls = consoleSpy.calls; + // Validate there is no class attribute hydration mismatch + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.html new file mode 100644 index 0000000000..7840cce9fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.scoped.css new file mode 100644 index 0000000000..d0bcad1033 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/child/child.scoped.css @@ -0,0 +1,3 @@ +:host { + padding: 8px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.html new file mode 100644 index 0000000000..4a8b50481d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.js new file mode 100644 index 0000000000..2aaa1dc37c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.js @@ -0,0 +1,6 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; + styles = 'yolo'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.scoped.css new file mode 100644 index 0000000000..1c50c3c426 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/dynamic-class/x/main/main.scoped.css @@ -0,0 +1,7 @@ +:host { + padding: 0; +} + +.yolo { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/index.spec.js new file mode 100644 index 0000000000..7662a615c4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/index.spec.js @@ -0,0 +1,8 @@ +export default { + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + hydrateComponent(target, Component, {}); + const consoleCalls = consoleSpy.calls; + // Validate there is no class attribute hydration mismatch + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.html new file mode 100644 index 0000000000..7840cce9fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.scoped.css new file mode 100644 index 0000000000..d0bcad1033 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/child/child.scoped.css @@ -0,0 +1,3 @@ +:host { + padding: 8px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.html new file mode 100644 index 0000000000..b9c4779b5f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.scoped.css new file mode 100644 index 0000000000..1c50c3c426 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/static-class/x/main/main.scoped.css @@ -0,0 +1,7 @@ +:host { + padding: 0; +} + +.yolo { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/index.spec.js new file mode 100644 index 0000000000..7662a615c4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/index.spec.js @@ -0,0 +1,8 @@ +export default { + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + hydrateComponent(target, Component, {}); + const consoleCalls = consoleSpy.calls; + // Validate there is no class attribute hydration mismatch + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.html new file mode 100644 index 0000000000..7840cce9fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.scoped.css new file mode 100644 index 0000000000..d0bcad1033 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/child/child.scoped.css @@ -0,0 +1,3 @@ +:host { + padding: 8px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.html new file mode 100644 index 0000000000..586e14e39f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.scoped.css new file mode 100644 index 0000000000..a962d1624d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/with-host-selector/without-class/x/main/main.scoped.css @@ -0,0 +1,3 @@ +:host { + padding: 0; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/index.spec.js new file mode 100644 index 0000000000..571159d9bf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/index.spec.js @@ -0,0 +1,23 @@ +export default { + snapshot(target) { + const lightChild = target.shadowRoot.querySelector('[data-id="x-light-child"]'); + + return { + lightParent: target, + parentText: target.shadowRoot.querySelector('[data-id="parent-text"]'), + shadowChild: lightChild, + childText: lightChild.querySelector('[data-id="child-text"]'), + }; + }, + test(target, snapshots) { + const hydratedSnapshot = this.snapshot(target); + + expect(hydratedSnapshot.lightParent).toBe(snapshots.lightParent); + expect(hydratedSnapshot.parentText).toBe(snapshots.parentText); + expect(hydratedSnapshot.shadowChild).toBe(snapshots.shadowChild); + expect(hydratedSnapshot.childText).toBe(snapshots.childText); + + expect(hydratedSnapshot.parentText.textContent).toEqual('inside parent'); + expect(hydratedSnapshot.childText.textContent).toEqual('inside child'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/lightChild/lightChild.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/lightChild/lightChild.html new file mode 100644 index 0000000000..6672768dfd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/lightChild/lightChild.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/lightChild/lightChild.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/lightChild/lightChild.js new file mode 100644 index 0000000000..4b92f22854 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/lightChild/lightChild.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class LightChild extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/main/main.html new file mode 100644 index 0000000000..9735d640a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/shadow-parent-light-child/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/index.spec.js new file mode 100644 index 0000000000..3d01fae07b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/index.spec.js @@ -0,0 +1,46 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.querySelector('x-with-slots'); + const cmpWithSlotParagraphs = cmpWithSlot.querySelectorAll('p'); + const [mainText, secondText] = cmpWithSlot.querySelectorAll('span'); + + return { + withSlot: cmpWithSlot, + mainText, + secondText, + cmpWithSlotParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.mainText).toBe(snapshots.mainText); + expect(snapshotAfterHydration.secondText).toBe(snapshots.secondText); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + + expect(snapshotAfterHydration.mainText.textContent).toBe('initial'); + + // let's verify handlers + snapshotAfterHydration.mainText.click(); + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + + expect(target.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(1); + + target.slotText = 'changed'; + + return Promise.resolve().then(() => { + const lateSnapshot = this.snapshot(target); + + expect(lateSnapshot.mainText.textContent).toBe('changed'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/main/main.html new file mode 100644 index 0000000000..f2f34b2ce3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/main/main.js new file mode 100644 index 0000000000..69b7b33d44 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/main/main.js @@ -0,0 +1,17 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/withSlots/withSlots.html new file mode 100644 index 0000000000..4463cb8744 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/withSlots/withSlots.js new file mode 100644 index 0000000000..31ef6c5a03 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/default/x/withSlots/withSlots.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/index.spec.js new file mode 100644 index 0000000000..3d01fae07b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/index.spec.js @@ -0,0 +1,46 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.querySelector('x-with-slots'); + const cmpWithSlotParagraphs = cmpWithSlot.querySelectorAll('p'); + const [mainText, secondText] = cmpWithSlot.querySelectorAll('span'); + + return { + withSlot: cmpWithSlot, + mainText, + secondText, + cmpWithSlotParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.mainText).toBe(snapshots.mainText); + expect(snapshotAfterHydration.secondText).toBe(snapshots.secondText); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + + expect(snapshotAfterHydration.mainText.textContent).toBe('initial'); + + // let's verify handlers + snapshotAfterHydration.mainText.click(); + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + + expect(target.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(1); + + target.slotText = 'changed'; + + return Promise.resolve().then(() => { + const lateSnapshot = this.snapshot(target); + + expect(lateSnapshot.mainText.textContent).toBe('changed'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/main/main.html new file mode 100644 index 0000000000..59614ab0c2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/main/main.html @@ -0,0 +1,10 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/main/main.js new file mode 100644 index 0000000000..69b7b33d44 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/main/main.js @@ -0,0 +1,17 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/withSlots/withSlots.html new file mode 100644 index 0000000000..d143936f61 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/withSlots/withSlots.js new file mode 100644 index 0000000000..31ef6c5a03 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/named/x/withSlots/withSlots.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/index.spec.js new file mode 100644 index 0000000000..29e6f0ef67 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/index.spec.js @@ -0,0 +1,41 @@ +export default { + props: {}, + snapshot(target) { + const cmpChild = target.querySelector('x-child'); + const cmpChildDiv = target.querySelector('x-child div'); + const [cmpScopedOuter, cmpScopedInner] = target.querySelectorAll('x-scoped'); + + return { + target, + cmpScopedOuter, + cmpScopedInner, + cmpChild, + cmpChildDiv, + }; + }, + advancedTest(target, { Component, hydrateComponent, consoleSpy, container, selector }) { + const snapshotBeforeHydration = this.snapshot(target); + hydrateComponent(target, Component, this.props); + const hydratedTarget = container.querySelector(selector); + const snapshotAfterHydration = this.snapshot(hydratedTarget); + + for (const snapshotKey of Object.keys(snapshotBeforeHydration)) { + expect(snapshotBeforeHydration[snapshotKey]) + .withContext( + `${snapshotKey} should be the same DOM element both before and after hydration` + ) + .toBe(snapshotAfterHydration[snapshotKey]); + } + + for (const snapshotKey of ['target', 'cmpScopedOuter', 'cmpScopedInner']) { + expect(snapshotBeforeHydration[snapshotKey].childNodes) + .withContext( + `${snapshotKey} should have the same number of child nodes before & after hydration` + ) + .toHaveSize(snapshotAfterHydration[snapshotKey].childNodes.length); + } + + expect(consoleSpy.calls.warn).toHaveSize(0); + expect(consoleSpy.calls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/child/child.html new file mode 100644 index 0000000000..dbbdadf497 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/child/child.js new file mode 100644 index 0000000000..cd310547e9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/main/main.html new file mode 100644 index 0000000000..59c9aab39e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/main/main.html @@ -0,0 +1,14 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/main/main.js new file mode 100644 index 0000000000..0f60e8b223 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/scoped/scoped.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/scoped/scoped.html new file mode 100644 index 0000000000..065c9b5d53 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/scoped/scoped.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/scoped/scoped.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/scoped/scoped.js new file mode 100644 index 0000000000..a14b4668fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested-and-scoped/x/scoped/scoped.js @@ -0,0 +1,12 @@ +import { LightningElement, api } from 'lwc'; + +export default class scoped extends LightningElement { + static renderMode = 'light'; + @api instance = 'unknown'; + + get scopedItem() { + return { + msg: `from-x-scoped-${this.instance}`, + }; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/index.spec.js new file mode 100644 index 0000000000..dc3a08be97 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/index.spec.js @@ -0,0 +1,54 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.querySelector('x-with-slots'); + const cmpChild = target.querySelector('x-child'); + const cmpWithSlotParagraphs = cmpWithSlot.querySelectorAll('p'); + const childParagraphs = cmpChild.querySelectorAll('p'); + + const [mainText, secondText] = cmpWithSlot.querySelectorAll('span'); + + return { + withSlot: cmpWithSlot, + cmpChild, + mainText, + secondText, + cmpWithSlotParagraphs, + childParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.mainText).toBe(snapshots.mainText); + expect(snapshotAfterHydration.secondText).toBe(snapshots.secondText); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + expect(snapshotAfterHydration.childParagraphs).toEqual(snapshots.childParagraphs); + + expect(snapshotAfterHydration.mainText.textContent).toBe('initial'); + + // let's verify handlers + snapshotAfterHydration.mainText.click(); + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + snapshotAfterHydration.childParagraphs[0].click(); + + expect(target.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.cmpChild.timesHandlerIsExecuted).toBe(1); + + target.slotText = 'changed'; + + return Promise.resolve().then(() => { + const lateSnapshot = this.snapshot(target); + + expect(lateSnapshot.mainText.textContent).toBe('changed'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/child/child.html new file mode 100644 index 0000000000..4463cb8744 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/child/child.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/child/child.js new file mode 100644 index 0000000000..7d2278eb5e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/child/child.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/main/main.html new file mode 100644 index 0000000000..0bb3927ebd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/main/main.html @@ -0,0 +1,10 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/main/main.js new file mode 100644 index 0000000000..69b7b33d44 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/main/main.js @@ -0,0 +1,17 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/withSlots/withSlots.html new file mode 100644 index 0000000000..4463cb8744 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/withSlots/withSlots.js new file mode 100644 index 0000000000..31ef6c5a03 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/nested/x/withSlots/withSlots.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/index.spec.js new file mode 100644 index 0000000000..a7b5f7bf0a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/index.spec.js @@ -0,0 +1,31 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.querySelector('x-with-slots'); + const cmpWithSlotParagraphs = cmpWithSlot.querySelectorAll('p'); + + return { + withSlot: cmpWithSlot, + cmpWithSlotParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toHaveSize(3); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + + // let's verify handlers + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + snapshotAfterHydration.cmpWithSlotParagraphs[1].click(); + + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(2); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/main/main.html new file mode 100644 index 0000000000..9be43c1622 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/main/main.js new file mode 100644 index 0000000000..69b7b33d44 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/main/main.js @@ -0,0 +1,17 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/withSlots/withSlots.html new file mode 100644 index 0000000000..9f3345a948 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/withSlots/withSlots.js new file mode 100644 index 0000000000..31ef6c5a03 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/slots/no-content/x/withSlots/withSlots.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + static renderMode = 'light'; + + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/index.spec.js new file mode 100644 index 0000000000..e59544e875 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('title')).toBe('client-title'); + expect(p.getAttribute('data-same')).toBe('same-value'); + expect(p.getAttribute('data-another-diff')).toBe('client-val'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: title="ssr-title" - expected on client: title="client-title"', + 'Hydration attribute mismatch on:

- rendered on server: data-another-diff="ssr-val" - expected on client: data-another-diff="client-val"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/x/main/main.html new file mode 100644 index 0000000000..72e815ac86 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/x/main/main.html @@ -0,0 +1,18 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/index.spec.js new file mode 100644 index 0000000000..00585b1f87 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + foo: 'server', + }, + clientProps: { + foo: 'client', + }, + snapshot(target) { + const div = target.shadowRoot.querySelector('div'); + return { + div, + }; + }, + test(target, snapshots, consoleCalls) { + const div = target.shadowRoot.querySelector('div'); + expect(div).not.toBe(snapshots.div); + expect(div.getAttribute('data-foo')).toBe('client'); + expect(div.getAttribute('data-static')).toBe('same-value'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: data-foo="server" - expected on client: data-foo="client"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/x/main/main.html new file mode 100644 index 0000000000..1a3f98cdd6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/x/main/main.js new file mode 100644 index 0000000000..869ac698f9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api foo; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/index.spec.js new file mode 100644 index 0000000000..57e77fa42c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/index.spec.js @@ -0,0 +1,29 @@ +export default { + props: { + classes: 'c1 c2 c3', + }, + clientProps: { + classes: 'c2 c3 c4', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.className).not.toBe(snapshots.classes); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="c1 c2 c3" - expected on client: class="c2 c3 c4"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/x/main/main.html new file mode 100644 index 0000000000..840c1025f9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/x/main/main.js new file mode 100644 index 0000000000..a9d7d6f286 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api classes; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/index.spec.js new file mode 100644 index 0000000000..d9662f2dd1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/index.spec.js @@ -0,0 +1,32 @@ +// SSR has no class at all, whereas the client has `class="null"`. +// This is to test if hydration is smart enough to recognize the difference between a null +// attribute and the literal string "null". +export default { + props: { + className: '', + }, + clientProps: { + className: 'null', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + className: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.className).not.toBe(snapshots.className); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="" - expected on client: class="null"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/x/main/main.html new file mode 100644 index 0000000000..850e13d2f7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/x/main/main.js new file mode 100644 index 0000000000..e6a58e407c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-empty-in-ssr-null-string-in-client/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api className; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/index.spec.js new file mode 100644 index 0000000000..ba5521c8b8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/index.spec.js @@ -0,0 +1,32 @@ +// SSR has `class="null"`, whereas the client has no class at all. +// This is to test if hydration is smart enough to recognize the difference between a null +// attribute and the literal string "null". +export default { + props: { + className: 'null', + }, + clientProps: { + className: undefined, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + className: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.className).not.toBe(snapshots.className); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="null" - expected on client: class=""', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/x/main/main.html new file mode 100644 index 0000000000..850e13d2f7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/x/main/main.js new file mode 100644 index 0000000000..e6a58e407c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-null-string-in-ssr-empty-in-client/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api className; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/index.spec.js new file mode 100644 index 0000000000..d60cecb71e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/index.spec.js @@ -0,0 +1,26 @@ +export default { + props: { + classes: 'c1 c2 c3', + }, + clientProps: { + classes: 'c3 c2 c1', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.className).toBe(snapshots.classes); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/x/main/main.html new file mode 100644 index 0000000000..840c1025f9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/x/main/main.js new file mode 100644 index 0000000000..a9d7d6f286 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same-different-order-does-not-throw/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api classes; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/index.spec.js new file mode 100644 index 0000000000..2b318e9d58 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/index.spec.js @@ -0,0 +1,18 @@ +export default { + props: { + classes: 'c1 c2 c3', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.className).toBe(snapshots.classes); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/x/main/main.html new file mode 100644 index 0000000000..840c1025f9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/x/main/main.js new file mode 100644 index 0000000000..a9d7d6f286 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-same/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api classes; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/index.spec.js new file mode 100644 index 0000000000..06fa19d842 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + classes: 'yolo', + }, + clientProps: { + classes: '', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).not.toBe(snapshots.p); + expect(p.className).toBe(''); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="yolo" - expected on client: class=""', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/x/main/main.html new file mode 100644 index 0000000000..213b575922 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/x/main/main.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/x/main/main.js new file mode 100644 index 0000000000..837fcfe574 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-client-nonempty-on-server/x/main/main.js @@ -0,0 +1,4 @@ +import { LightningElement, api } from 'lwc'; +export default class Main extends LightningElement { + @api classes; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/index.spec.js new file mode 100644 index 0000000000..cf15d194a8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + classes: '', + }, + clientProps: { + classes: 'yolo', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).not.toBe(snapshots.p); + expect(p.className).toBe('yolo'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="" - expected on client: class="yolo"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/x/main/main.html new file mode 100644 index 0000000000..213b575922 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/x/main/main.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/x/main/main.js new file mode 100644 index 0000000000..837fcfe574 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string-on-server-nonempty-on-client/x/main/main.js @@ -0,0 +1,4 @@ +import { LightningElement, api } from 'lwc'; +export default class Main extends LightningElement { + @api classes; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/index.spec.js new file mode 100644 index 0000000000..6766da610d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + classes: '', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.className).toBe(snapshots.classes); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/x/main/main.html new file mode 100644 index 0000000000..213b575922 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/x/main/main.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/x/main/main.js new file mode 100644 index 0000000000..837fcfe574 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/x/main/main.js @@ -0,0 +1,4 @@ +import { LightningElement, api } from 'lwc'; +export default class Main extends LightningElement { + @api classes; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/index.spec.js new file mode 100644 index 0000000000..c212253276 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.className).not.toBe(snapshots.classes); + expect(p.className).toBe('c1 c2 c3'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="c1 c3" - expected on client: class="c1 c2 c3"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/x/main/main.html new file mode 100644 index 0000000000..80f48b8b0f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/index.spec.js new file mode 100644 index 0000000000..8a4869acb9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.className).not.toBe(snapshots.classes); + expect(p.className).toBe('c1 c3'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="c1 c2 c3" - expected on client: class="c1 c3"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/x/main/main.html new file mode 100644 index 0000000000..b7da6683e3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/index.spec.js new file mode 100644 index 0000000000..6915109a14 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/index.spec.js @@ -0,0 +1,22 @@ +export default { + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + // This simulates a condition where the server-rendered markup has + // a classname that is incorrectly missing in the client-side + // VDOM at the time of validation. + // + // Outside of this test, the tested condition should never be reached + // unless something in SSR or hydration logic is broken. + target.shadowRoot.querySelector('x-child').classList.add('foo'); + + hydrateComponent(target, Component, {}); + + const consoleCalls = consoleSpy.calls; + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: class="foo" - expected on client: class=""', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/child/child.html new file mode 100644 index 0000000000..1b8ebb63c6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/child/child.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/main/main.html new file mode 100644 index 0000000000..fa8171b8f8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/main/main.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/index.spec.js new file mode 100644 index 0000000000..ea7c87d29f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/index.spec.js @@ -0,0 +1,37 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + // TODO [#4656]: static optimization causes mismatches for style/class only when ordering is different + if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) { + expect(p).toBe(snapshots.p); + expect(p.className).toBe(snapshots.classes); + + expect(consoleCalls.warn).toHaveSize(0); + } else { + expect(p).not.toBe(snapshots.p); + expect(p.className).toBe('c1 c2 c3'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: class="c3 c2 c1" - expected on client: class="c1 c2 c3"', + 'Hydration completed with errors.', + ], + }); + } + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/x/main/main.html new file mode 100644 index 0000000000..915b6893b3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/index.spec.js new file mode 100644 index 0000000000..4ce3cf9fa4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/index.spec.js @@ -0,0 +1,20 @@ +export default { + props: { + s1: 's1', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.className).toBe(snapshots.classes); + // static classes are skipped by hydration validation + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/x/main/main.html new file mode 100644 index 0000000000..c721460f6d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/x/main/main.js new file mode 100644 index 0000000000..c72a9e82b5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-with-static-parts/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api s1; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/index.spec.js new file mode 100644 index 0000000000..a69d00963d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/index.spec.js @@ -0,0 +1,15 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + classes: p.className, + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.className).toBe(snapshots.classes); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/x/main/main.html new file mode 100644 index 0000000000..137d2be8e4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/index.spec.js new file mode 100644 index 0000000000..6abb8ac3a8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/index.spec.js @@ -0,0 +1,22 @@ +export default { + snapshot(target) { + const [div1, div2] = target.shadowRoot.querySelectorAll('div'); + return { + div1, + div2, + }; + }, + test(target, snapshots, consoleCalls) { + const [div1, div2] = target.shadowRoot.querySelectorAll('div'); + + expect(div1).toBe(snapshots.div1); + expect(div2).toBe(snapshots.div2); + + // TODO [#4714]: Scope token classes render in an inconsistent order for static vs dynamic classes + expect(new Set(div1.classList)).toEqual(new Set(['foo', 'lwc-6958o7oup43'])); + expect(new Set(div2.classList)).toEqual(new Set(['bar', 'lwc-6958o7oup43'])); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.html new file mode 100644 index 0000000000..82a80e8240 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.js new file mode 100644 index 0000000000..ee02424c67 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + clazz = 'bar'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.scoped.css new file mode 100644 index 0000000000..b80d2f3062 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/scoped-styles-with-existing-class/x/main/main.scoped.css @@ -0,0 +1,3 @@ +div { + background-color: wheat; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/index.spec.js new file mode 100644 index 0000000000..dfc078cd9f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + showAsText: true, + }, + clientProps: { + showAsText: false, + }, + snapshot(target) { + const text = target.shadowRoot.firstChild; + return { + text, + }; + }, + test(target, snapshots, consoleCalls) { + const comment = target.shadowRoot.firstChild; + + expect(comment.nodeType).toBe(Node.COMMENT_NODE); + expect(comment.nodeValue).toBe(snapshots.text.nodeValue); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration node mismatch on: #comment - rendered on server: #text - expected on client: #comment', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/x/main/main.html new file mode 100644 index 0000000000..c6bfcf022e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/x/main/main.js new file mode 100644 index 0000000000..edfb80773a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showAsText; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/index.spec.js new file mode 100644 index 0000000000..461ce34b5a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/index.spec.js @@ -0,0 +1,37 @@ +export default { + props: { + content: '

test-content

', + }, + clientProps: { + content: '

different-content

', + }, + snapshot(target) { + const div = target.shadowRoot.querySelector('div'); + const p = div.querySelector('p'); + return { + div, + p, + }; + }, + test(target, snapshot, consoleCalls) { + const div = target.shadowRoot.querySelector('div'); + const p = div.querySelector('p'); + + expect(div).toBe(snapshot.div); + expect(p).not.toBe(snapshot.p); + expect(p.textContent).toBe('different-content'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration innerHTML mismatch on:
- rendered on server:

test-content

- expected on client:

different-content

', + ], + }); + + target.content = '

another-content

'; + + return Promise.resolve().then(() => { + expect(div.textContent).toBe('another-content'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/x/main/main.html new file mode 100644 index 0000000000..66a8e2c720 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/x/main/main.js new file mode 100644 index 0000000000..8066dd4ab7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api content; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/index.spec.js new file mode 100644 index 0000000000..13167f0a20 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/index.spec.js @@ -0,0 +1,36 @@ +export default { + props: { + classes: 'ssr-class', + styles: 'background-color: red;', + attrs: 'ssr-attrs', + }, + clientProps: { + classes: 'client-class', + styles: 'background-color: blue;', + attrs: 'client-attrs', + }, + snapshot(target) { + return { + p: target.shadowRoot.querySelector('p'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + + expect(p.className).toBe('client-class'); + expect(p.getAttribute('style')).toBe('background-color: blue;'); + expect(p.getAttribute('data-attrs')).toBe('client-attrs'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

- rendered on server: data-attrs="ssr-attrs" - expected on client: data-attrs="client-attrs"', + 'Hydration attribute mismatch on:

- rendered on server: class="ssr-class" - expected on client: class="client-class"', + 'Hydration attribute mismatch on:

- rendered on server: style="background-color: red;" - expected on client: style="background-color: blue;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/x/main/main.html new file mode 100644 index 0000000000..046a32c620 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/x/main/main.js new file mode 100644 index 0000000000..f73a8b980c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api classes; + @api styles; + @api attrs; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/index.spec.js new file mode 100644 index 0000000000..658111cb33 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + ctor: 'server', + }, + clientProps: { + ctor: 'client', + }, + snapshot(target) { + const cmp = target.shadowRoot.querySelector('x-server'); + return { + tagName: cmp.tagName.toLowerCase(), + }; + }, + test(target, snapshots, consoleCalls) { + // Server side constructor + expect(snapshots.tagName).toBe('x-server'); + // Client side constructor + expect(target.shadowRoot.querySelector('x-client')).not.toBeNull(); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration node mismatch on: - rendered on server: - expected on client: ', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/client/client.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/client/client.html new file mode 100644 index 0000000000..d8473e9580 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/client/client.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/client/client.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/client/client.js new file mode 100644 index 0000000000..3f0d7e7e52 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/client/client.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/main/main.html new file mode 100644 index 0000000000..f1bb24a788 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/main/main.js new file mode 100644 index 0000000000..2c262e24f7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/main/main.js @@ -0,0 +1,21 @@ +import { LightningElement, api } from 'lwc'; +import ServerCtor from 'x/server'; +import ClientCtor from 'x/client'; + +const ctors = { + server: ServerCtor, + client: ClientCtor, +}; + +export default class Main extends LightningElement { + #ctor; + + @api + set ctor(name) { + this.#ctor = ctors[name]; + } + + get ctor() { + return this.#ctor; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/server/server.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/server/server.html new file mode 100644 index 0000000000..2acfbad4a7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/server/server.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/server/server.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/server/server.js new file mode 100644 index 0000000000..3f0d7e7e52 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/x/server/server.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/index.spec.js new file mode 100644 index 0000000000..4e928f311c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + showAsText: false, + }, + clientProps: { + showAsText: true, + }, + snapshot(target) { + return { + text: target.shadowRoot.firstChild.textContent, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + + expect(hydratedSnapshot.text).toBe(snapshots.text); + + const text = target.shadowRoot.firstChild; + + expect(text.nodeType).toBe(Node.TEXT_NODE); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration node mismatch on: #text - rendered on server: - expected on client: #text', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/x/main/main.html new file mode 100644 index 0000000000..84e922c725 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/x/main/main.js new file mode 100644 index 0000000000..edfb80773a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showAsText; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/index.spec.js new file mode 100644 index 0000000000..da46c27c61 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + firstText: p.childNodes[0], + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshots = this.snapshot(target); + + expect(snapshots.p).toBe(hydratedSnapshots.p); + expect(snapshots.firstText).toBe(hydratedSnapshots.firstText); + + expect(consoleCalls.error).toHaveSize(0); + expect(consoleCalls.warn).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/x/main/main.html new file mode 100644 index 0000000000..d1b2d05ef3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/x/main/main.js new file mode 100644 index 0000000000..d9a989fbcc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/empty-nodes/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + get emptyText() { + return ''; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/index.spec.js new file mode 100644 index 0000000000..f875575bfd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + showFirstComment: true, + }, + clientProps: { + showFirstComment: false, + }, + snapshot(target) { + const comment = target.shadowRoot.firstChild; + return { + comment, + commentText: comment.nodeValue, + }; + }, + test(target, snapshots, consoleCalls) { + const comment = target.shadowRoot.firstChild; + expect(comment).toBe(snapshots.comment); + expect(comment.nodeValue).not.toBe(snapshots.commentText); + expect(comment.nodeValue).toBe('second'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration comment mismatch on: #comment - rendered on server: first - expected on client: second', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/x/main/main.html new file mode 100644 index 0000000000..2fa6b76597 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/x/main/main.js new file mode 100644 index 0000000000..cd370b7c74 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showFirstComment; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js new file mode 100644 index 0000000000..188978106f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js @@ -0,0 +1,36 @@ +export default { + props: { + greeting: 'hello!', + }, + clientProps: { + greeting: 'bye!', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('bye!'); + if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on: #text - rendered on server: hello! - expected on client: bye!', + ], + }); + } else { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on:

- rendered on server: hello! - expected on client: bye!', + ], + }); + } + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/x/main/main.html new file mode 100644 index 0000000000..fd4547d4f7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/x/main/main.js new file mode 100644 index 0000000000..aa674ccf41 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api greeting; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/index.spec.js new file mode 100644 index 0000000000..63d8c26a72 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/index.spec.js @@ -0,0 +1,35 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const child = target.shadowRoot.querySelector('x-child'); + const div = child.shadowRoot.querySelector('div'); + + return { + child, + div, + }; + }, + test(target, snapshots, consoleCalls) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.child).not.toBe(snapshots.child); + expect(snapshotAfterHydration.div).not.toBe(snapshots.div); + + const { child } = snapshotAfterHydration; + expect(child.getAttribute('data-foo')).toBe('bar'); + expect(child.getAttribute('data-mutatis')).toBe('mutandis'); + expect(child.getAttribute('class')).toBe('is-client'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: class="is-server" - expected on client: class="is-client"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/child/child.html new file mode 100644 index 0000000000..b54169fa06 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/child/child.js new file mode 100644 index 0000000000..e9c7cbaa9c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + yolo = 'woot'; + + connectedCallback() { + this.setAttribute('data-mutatis', 'mutandis'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/main/main.html new file mode 100644 index 0000000000..3c9cf33c02 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/main/main.js new file mode 100644 index 0000000000..aacd9d30f5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr-mutated-class-mismatch/x/main/main.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; + + get mismatchingClass() { + return this.ssr ? 'is-server' : 'is-client'; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/index.spec.js new file mode 100644 index 0000000000..00065f381a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: {}, + snapshot(target) { + const div = target.shadowRoot.querySelector('x-child').shadowRoot.querySelector('div'); + + return { + div, + }; + }, + test(target, snapshots, consoleCalls) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.div).toBe(snapshots.div); + const child = target.shadowRoot.querySelector('x-child'); + expect(child.getAttribute('data-foo')).toBe('bar'); + expect(child.getAttribute('data-mutatis')).toBe('mutandis'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/child/child.html new file mode 100644 index 0000000000..b54169fa06 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/child/child.js new file mode 100644 index 0000000000..e9c7cbaa9c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + yolo = 'woot'; + + connectedCallback() { + this.setAttribute('data-mutatis', 'mutandis'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/main/main.html new file mode 100644 index 0000000000..23344b575b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/index.spec.js new file mode 100644 index 0000000000..a63010e4d2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/index.spec.js @@ -0,0 +1,34 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const child = target.shadowRoot.querySelector('x-child'); + const div = child.shadowRoot.querySelector('div'); + + return { + child, + div, + }; + }, + test(target, snapshots, consoleCalls) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.child).not.toBe(snapshots.child); + expect(snapshotAfterHydration.div).not.toBe(snapshots.div); + + const { child } = snapshotAfterHydration; + expect(child.getAttribute('class')).toBe('static mutatis'); + expect(child.getAttribute('data-mismatched-attr')).toBe('is-client'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: data-mismatched-attr="is-server" - expected on client: data-mismatched-attr="is-client"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/child/child.html new file mode 100644 index 0000000000..b54169fa06 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/child/child.js new file mode 100644 index 0000000000..92e1a94698 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/child/child.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + yolo = 'woot'; + + connectedCallback() { + this.classList.add('mutatis'); + this.classList.add('mutandis'); + this.classList.remove('mutandis'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/main/main.html new file mode 100644 index 0000000000..f53301c9d0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/main/main.js new file mode 100644 index 0000000000..05fbc211ee --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch/x/main/main.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; + + get mismatchedAttr() { + return this.ssr ? 'is-server' : 'is-client'; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/index.spec.js new file mode 100644 index 0000000000..2804139cdb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: {}, + snapshot(target) { + const div = target.shadowRoot.querySelector('x-child').shadowRoot.querySelector('div'); + + return { + div, + }; + }, + test(target, snapshots, consoleCalls) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.div).toBe(snapshots.div); + expect(target.shadowRoot.querySelector('x-child').getAttribute('class')).toBe( + 'static mutatis' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/child/child.html new file mode 100644 index 0000000000..b54169fa06 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/child/child.js new file mode 100644 index 0000000000..92e1a94698 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/child/child.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + yolo = 'woot'; + + connectedCallback() { + this.classList.add('mutatis'); + this.classList.add('mutandis'); + this.classList.remove('mutandis'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/main/main.html new file mode 100644 index 0000000000..dfc8fe9982 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/index.spec.js new file mode 100644 index 0000000000..eeb6a10ceb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/index.spec.js @@ -0,0 +1,20 @@ +export default { + props: {}, + snapshot(target) { + const div = target.shadowRoot.querySelector('x-child').shadowRoot.querySelector('div'); + + return { + div, + }; + }, + test(target, snapshots, consoleCalls) { + const snapshotAfterHydration = this.snapshot(target); + expect(snapshotAfterHydration.div).toBe(snapshots.div); + expect(target.shadowRoot.querySelector('x-child').style.color).toBe('blue'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/child/child.html new file mode 100644 index 0000000000..b54169fa06 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/child/child.js new file mode 100644 index 0000000000..1fd2cb1d62 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + yolo = 'woot'; + + connectedCallback() { + this.setAttribute('style', 'color: blue;'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/main/main.html new file mode 100644 index 0000000000..ecf47151fa --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/index.spec.js new file mode 100644 index 0000000000..ebd16f0595 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/index.spec.js @@ -0,0 +1,26 @@ +export default { + props: { + colors: ['red', 'blue', 'green'], + }, + clientProps: { + colors: ['red', 'blue'], + }, + snapshot(target) { + return { + text: target.shadowRoot.firstChild.innerText, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + + expect(hydratedSnapshot.text).not.toBe(snapshots.text); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration child node mismatch on:

    - rendered on server:
  • ,
  • ,
  • - expected on client:
  • ,
  • ', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/x/main/main.html new file mode 100644 index 0000000000..788d09d887 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/x/main/main.js new file mode 100644 index 0000000000..88c9d7ed9d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api colors; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js new file mode 100644 index 0000000000..14632e1b57 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js @@ -0,0 +1,37 @@ +export default { + props: { + showBlue: true, + }, + clientProps: { + showBlue: false, + }, + snapshot(target) { + return { + text: target.shadowRoot.firstChild.innerText, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + + expect(hydratedSnapshot.text).not.toBe(snapshots.text); + + if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on: #text - rendered on server: blue - expected on client: green', + 'Hydration child node mismatch on:
      - rendered on server:
    • ,
    • ,
    • - expected on client:
    • ,,
    • ', + 'Hydration completed with errors.', + ], + }); + } else { + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration child node mismatch on:
        - rendered on server:
      • ,
      • ,
      • - expected on client:
      • ,,
      • ', + 'Hydration completed with errors.', + ], + }); + } + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/x/main/main.html new file mode 100644 index 0000000000..8e7c249f44 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/x/main/main.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/x/main/main.js new file mode 100644 index 0000000000..b536e4afcd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showBlue; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/index.spec.js new file mode 100644 index 0000000000..ae3cfa8bb2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/index.spec.js @@ -0,0 +1,22 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + title: p.title, + ssrOnlyAttr: p.getAttribute('data-ssr-only'), + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.title).toBe(snapshots.title); + expect(p.getAttribute('data-ssr-only')).toBe(snapshots.ssrOnlyAttr); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/x/main/main.html new file mode 100644 index 0000000000..461836b6b8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/x/main/main.html @@ -0,0 +1,15 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/preserve-ssr-attr/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/index.spec.js new file mode 100644 index 0000000000..7c3c8d9ff3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/index.spec.js @@ -0,0 +1,27 @@ +export default { + props: { + showMe: false, + }, + clientProps: { + showMe: true, + }, + snapshot(target) { + return { + div: target.shadowRoot.querySelector('div'), + }; + }, + test(target, snapshots, consoleCalls) { + const div = target.shadowRoot.querySelector('div'); + + expect(snapshots.div).toBeNull(); + expect(div).toBeDefined(); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration child node mismatch on: #document-fragment - rendered on server: - expected on client: #comment', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/x/main/main.html new file mode 100644 index 0000000000..76734f3e77 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/x/main/main.js new file mode 100644 index 0000000000..727ba9540d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/x/main/main.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showMe; + + __renderedOnce = false; + + renderedCallback() { + if (!this.__renderedOnce) { + this.__renderedOnce = true; + this.showMe = false; + } + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/index.spec.js new file mode 100644 index 0000000000..117ea9bf41 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/index.spec.js @@ -0,0 +1,32 @@ +export default { + props: { + dynamicStyle: 'background-color: red; border-color: red;', + }, + clientProps: { + dynamicStyle: 'background-color: red; border-color: red !important;', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).not.toBe(snapshots.style); + expect(p.getAttribute('style')).toBe( + 'background-color: red; border-color: red !important;' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red;" - expected on client: style="background-color: red; border-color: red !important;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/x/main/main.html new file mode 100644 index 0000000000..16a9ea3139 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/x/main/main.js new file mode 100644 index 0000000000..d85340d1f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api dynamicStyle; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/index.spec.js new file mode 100644 index 0000000000..5d843688be --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/index.spec.js @@ -0,0 +1,32 @@ +export default { + props: { + dynamicStyle: 'background-color: red; border-color: red;', + }, + clientProps: { + dynamicStyle: 'background-color: red; border-color: red; margin: 1px;', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).not.toBe(snapshots.style); + expect(p.getAttribute('style')).toBe( + 'background-color: red; border-color: red; margin: 1px;' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red;" - expected on client: style="background-color: red; border-color: red; margin: 1px;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/x/main/main.html new file mode 100644 index 0000000000..16a9ea3139 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/x/main/main.js new file mode 100644 index 0000000000..d85340d1f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api dynamicStyle; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/index.spec.js new file mode 100644 index 0000000000..ca92f666fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + dynamicStyle: 'background-color: red; border-color: red; margin: 1px;', + }, + clientProps: { + dynamicStyle: 'background-color: red; border-color: red;', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).not.toBe(snapshots.style); + expect(p.getAttribute('style')).toBe('background-color: red; border-color: red;'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red; margin: 1px;" - expected on client: style="background-color: red; border-color: red;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/x/main/main.html new file mode 100644 index 0000000000..16a9ea3139 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/x/main/main.js new file mode 100644 index 0000000000..d85340d1f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api dynamicStyle; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/index.spec.js new file mode 100644 index 0000000000..a9f3d09d8a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/index.spec.js @@ -0,0 +1,31 @@ +export default { + props: { + dynamicStyle: 'background-color: red; border-color: red; margin: 1px;', + }, + clientProps: { + dynamicStyle: 'margin: 1px; border-color: red; background-color: red;', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).toBe( + 'margin: 1px; border-color: red; background-color: red;' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red; margin: 1px;" - expected on client: style="margin: 1px; border-color: red; background-color: red;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/x/main/main.html new file mode 100644 index 0000000000..16a9ea3139 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/x/main/main.js new file mode 100644 index 0000000000..d85340d1f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api dynamicStyle; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/index.spec.js new file mode 100644 index 0000000000..06a355fde7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/index.spec.js @@ -0,0 +1,15 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/x/main/main.html new file mode 100644 index 0000000000..16a9ea3139 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/x/main/main.js new file mode 100644 index 0000000000..a77cebf3ac --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-priority/x/main/main.js @@ -0,0 +1,6 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + // Note: extra spaces matters + dynamicStyle = 'background-color: red; border-color: red !important ; '; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/index.spec.js new file mode 100644 index 0000000000..fd3dbd4c66 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/index.spec.js @@ -0,0 +1,17 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/x/main/main.html new file mode 100644 index 0000000000..16a9ea3139 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/x/main/main.js new file mode 100644 index 0000000000..6c6ffd4122 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api dynamicStyle = 'background-color: red; border-color: red; margin: 1px;'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/index.spec.js new file mode 100644 index 0000000000..6648a2a90a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + styles: 'color: burlywood;', + }, + clientProps: { + styles: '', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + styles: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(null); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="color: burlywood;" - expected on client: style=""', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/x/main/main.html new file mode 100644 index 0000000000..cf420d8b80 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/x/main/main.js new file mode 100644 index 0000000000..5fde8a72d8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-client-nonempty-on-server/x/main/main.js @@ -0,0 +1,4 @@ +import { LightningElement, api } from 'lwc'; +export default class Main extends LightningElement { + @api styles; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/index.spec.js new file mode 100644 index 0000000000..aca494470d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + styles: '', + }, + clientProps: { + styles: 'color: burlywood;', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + styles: p.className, + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).toBe('color: burlywood;'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="" - expected on client: style="color: burlywood;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/x/main/main.html new file mode 100644 index 0000000000..cf420d8b80 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/x/main/main.js new file mode 100644 index 0000000000..5fde8a72d8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/empty-string/empty-on-server-nonempty-on-client/x/main/main.js @@ -0,0 +1,4 @@ +import { LightningElement, api } from 'lwc'; +export default class Main extends LightningElement { + @api styles; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/index.spec.js new file mode 100644 index 0000000000..80b6824fc4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/index.spec.js @@ -0,0 +1,32 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).not.toBe(snapshots.style); + expect(p.getAttribute('style')).toBe( + 'background-color: red; border-color: red !important;' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red;" - expected on client: style="background-color: red; border-color: red !important;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/x/main/main.html new file mode 100644 index 0000000000..a8bc6469a5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/index.spec.js new file mode 100644 index 0000000000..89f82ca68a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/index.spec.js @@ -0,0 +1,32 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).not.toBe(snapshots.style); + expect(p.getAttribute('style')).toBe( + 'background-color: red; border-color: red; margin: 1px;' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red;" - expected on client: style="background-color: red; border-color: red; margin: 1px;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/x/main/main.html new file mode 100644 index 0000000000..81671733e8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/index.spec.js new file mode 100644 index 0000000000..f8bf2b3f88 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).not.toBe(snapshots.style); + expect(p.getAttribute('style')).toBe('background-color: red; border-color: red;'); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red; margin: 1px;" - expected on client: style="background-color: red; border-color: red;"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/x/main/main.html new file mode 100644 index 0000000000..17340f9969 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/index.spec.js new file mode 100644 index 0000000000..6c8fa727f2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/index.spec.js @@ -0,0 +1,39 @@ +export default { + props: { + ssr: true, + }, + clientProps: { + ssr: false, + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + // TODO [#4656]: static optimization causes mismatches for style/class only when ordering is different + if (process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION) { + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + + expect(consoleCalls.warn).toHaveSize(0); + } else { + expect(p).not.toBe(snapshots.p); + expect(p.getAttribute('style')).toBe( + 'margin: 1px; border-color: red; background-color: red;' + ); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on:

        - rendered on server: style="background-color: red; border-color: red; margin: 1px;" - expected on client: style="margin: 1px; border-color: red; background-color: red;"', + 'Hydration completed with errors.', + ], + }); + } + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/x/main/main.html new file mode 100644 index 0000000000..73e2ad235f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/x/main/main.js new file mode 100644 index 0000000000..8d248b4077 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api ssr; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/index.spec.js new file mode 100644 index 0000000000..06a355fde7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/index.spec.js @@ -0,0 +1,15 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/x/main/main.html new file mode 100644 index 0000000000..d427999eb8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-priority/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/index.spec.js new file mode 100644 index 0000000000..fc0d9aa2dd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/index.spec.js @@ -0,0 +1,20 @@ +export default { + props: { + c1: 'c1', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + // static classes are skipped by hydration validation + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/x/main/main.html new file mode 100644 index 0000000000..13ba37298d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/x/main/main.js new file mode 100644 index 0000000000..8f11610826 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-with-static-parts/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api c1; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/index.spec.js new file mode 100644 index 0000000000..fd3dbd4c66 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/index.spec.js @@ -0,0 +1,17 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('p'); + + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/x/main/main.html new file mode 100644 index 0000000000..120071ff36 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/index.spec.js new file mode 100644 index 0000000000..41ee06431f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/index.spec.js @@ -0,0 +1,17 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('div'); + return { + p, + style: p.getAttribute('style'), + }; + }, + test(target, snapshots, consoleCalls) { + const p = target.shadowRoot.querySelector('div'); + + expect(p).toBe(snapshots.p); + expect(p.getAttribute('style')).toBe(snapshots.style); + + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/x/main/main.html new file mode 100644 index 0000000000..c0802c1e54 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/x/main/main.js new file mode 100644 index 0000000000..47868e575e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/with-newlines/x/main/main.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + get customStyles() { + return ` + color: blue; + margin: 16px; + `; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/index.spec.js new file mode 100644 index 0000000000..ff771b9486 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/index.spec.js @@ -0,0 +1,28 @@ +export default { + props: { + showAsText: false, + }, + clientProps: { + showAsText: true, + }, + snapshot(target) { + const comment = target.shadowRoot.firstChild; + return { + comment, + }; + }, + test(target, snapshots, consoleCalls) { + const text = target.shadowRoot.firstChild; + + expect(text.nodeType).toBe(Node.TEXT_NODE); + expect(text.nodeValue).toBe(snapshots.comment.nodeValue); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration node mismatch on: #text - rendered on server: #comment - expected on client: #text', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/x/main/main.html new file mode 100644 index 0000000000..c6bfcf022e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/x/main/main.js new file mode 100644 index 0000000000..edfb80773a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showAsText; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/index.spec.js new file mode 100644 index 0000000000..677f8106d9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/index.spec.js @@ -0,0 +1,30 @@ +export default { + props: { + showAsText: true, + }, + clientProps: { + showAsText: false, + }, + snapshot(target) { + return { + text: target.shadowRoot.firstChild.textContent, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + + expect(hydratedSnapshot.text).toBe(snapshots.text); + + const text = target.shadowRoot.firstChild; + + expect(text.nodeType).toBe(Node.ELEMENT_NODE); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration node mismatch on: - rendered on server: #text - expected on client: ', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/x/main/main.html new file mode 100644 index 0000000000..84e922c725 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/x/main/main.js new file mode 100644 index 0000000000..edfb80773a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api showAsText; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/index.spec.js new file mode 100644 index 0000000000..6bfc2471c3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/index.spec.js @@ -0,0 +1,20 @@ +export default { + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots, consoleCalls) { + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + + expect(p.textContent).toBe('123'); + expect(p.getAttribute('data-attr')).toBe('123'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/x/main/main.html new file mode 100644 index 0000000000..f27a8e93f0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/x/main/main.js new file mode 100644 index 0000000000..202b4a5835 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/type-coercion-to-string/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement { + num = 123; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/index.spec.js new file mode 100644 index 0000000000..7b056430a5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + foo: target.shadowRoot.firstChild.firstChild.getAttribute('foo'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.foo).toBe(snapshots.foo); + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/child/child.html new file mode 100644 index 0000000000..a59f61212d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/child/child.js new file mode 100644 index 0000000000..64a85c2284 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/child/child.js @@ -0,0 +1,10 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + @api foo; + static validationOptOut = true; + + connectedCallback() { + this.setAttribute('foo', 'something else'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/main/main.html new file mode 100644 index 0000000000..2f75778d47 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/attr-mismatch/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/index.spec.js new file mode 100644 index 0000000000..93197d4fdf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/index.spec.js @@ -0,0 +1,6 @@ +export default { + test(_target, _snapshots, consoleCalls) { + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/child/child.html new file mode 100644 index 0000000000..a59f61212d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/child/child.js new file mode 100644 index 0000000000..c9e3a9b1cb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = ['foo']; + + connectedCallback() { + this.classList.add('bar'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/main/main.html new file mode 100644 index 0000000000..a51716a474 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-no-opt-out/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/index.spec.js new file mode 100644 index 0000000000..1d50ad1135 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + classes: target.shadowRoot.firstChild.firstChild.className, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.classes).toBe(snapshots.classes); + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/child/child.html new file mode 100644 index 0000000000..a59f61212d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/child/child.js new file mode 100644 index 0000000000..d8b8eb0b62 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = ['class']; + + connectedCallback() { + this.classList.add('bar'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/main/main.html new file mode 100644 index 0000000000..a51716a474 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch-only/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/index.spec.js new file mode 100644 index 0000000000..1d50ad1135 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + classes: target.shadowRoot.firstChild.firstChild.className, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.classes).toBe(snapshots.classes); + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/child/child.html new file mode 100644 index 0000000000..a59f61212d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/child/child.js new file mode 100644 index 0000000000..49bd879b8f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = true; + + connectedCallback() { + this.classList.add('bar'); + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/main/main.html new file mode 100644 index 0000000000..a51716a474 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/class-mismatch/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/index.spec.js new file mode 100644 index 0000000000..c7de6e2d5f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/index.spec.js @@ -0,0 +1,19 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).not.toBe(snapshots.child); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: data-mutate-during-render="true" - expected on client: data-mutate-during-render="false"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/child/child.js new file mode 100644 index 0000000000..d8f37284cb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/child/child.js @@ -0,0 +1,13 @@ +import { LightningElement } from 'lwc'; +import template from './template.html'; + +export default class extends LightningElement { + connectedCallback() { + this.setAttribute('data-mutate-during-connected-callback', 'true'); + } + + render() { + this.setAttribute('data-mutate-during-render', 'true'); + return template; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/child/template.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/child/template.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/child/template.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/main/main.html new file mode 100644 index 0000000000..c9e761b068 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/main/main.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/no-opt-out/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/index.spec.js new file mode 100644 index 0000000000..f98157ec27 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).toBe(snapshots.child); + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/child/child.js new file mode 100644 index 0000000000..de5707d476 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/child/child.js @@ -0,0 +1,15 @@ +import { LightningElement } from 'lwc'; +import template from './template.html'; + +export default class extends LightningElement { + static validationOptOut = ['data-mutate-during-render']; + + connectedCallback() { + this.setAttribute('data-mutate-during-connected-callback', 'true'); + } + + render() { + this.setAttribute('data-mutate-during-render', 'true'); + return template; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/child/template.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/child/template.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/child/template.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/main/main.html new file mode 100644 index 0000000000..c9e761b068 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/main/main.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-array/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/index.spec.js new file mode 100644 index 0000000000..f98157ec27 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).toBe(snapshots.child); + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/child/child.js new file mode 100644 index 0000000000..bf59d66dac --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/child/child.js @@ -0,0 +1,15 @@ +import { LightningElement } from 'lwc'; +import template from './template.html'; + +export default class extends LightningElement { + static validationOptOut = true; + + connectedCallback() { + this.setAttribute('data-mutate-during-connected-callback', 'true'); + } + + render() { + this.setAttribute('data-mutate-during-render', 'true'); + return template; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/child/template.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/child/template.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/child/template.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/main/main.html new file mode 100644 index 0000000000..c9e761b068 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/main/main.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-true/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/index.spec.js new file mode 100644 index 0000000000..c7de6e2d5f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/index.spec.js @@ -0,0 +1,19 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).not.toBe(snapshots.child); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: data-mutate-during-render="true" - expected on client: data-mutate-during-render="false"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/child/child.js new file mode 100644 index 0000000000..56be139d76 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/child/child.js @@ -0,0 +1,15 @@ +import { LightningElement } from 'lwc'; +import template from './template.html'; + +export default class extends LightningElement { + static validationOptOut = ['does-not-exist']; + + connectedCallback() { + this.setAttribute('data-mutate-during-connected-callback', 'true'); + } + + render() { + this.setAttribute('data-mutate-during-render', 'true'); + return template; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/child/template.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/child/template.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/child/template.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/main/main.html new file mode 100644 index 0000000000..c9e761b068 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/main/main.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/mutate-in-connected-and-render/opt-out-wrong-array/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/index.spec.js new file mode 100644 index 0000000000..1d50ad1135 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/index.spec.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + classes: target.shadowRoot.firstChild.firstChild.className, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.classes).toBe(snapshots.classes); + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/child/child.html new file mode 100644 index 0000000000..a59f61212d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/child/child.js new file mode 100644 index 0000000000..0843574942 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = true; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/main/main.html new file mode 100644 index 0000000000..a51716a474 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/no-mutation/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/index.spec.js new file mode 100644 index 0000000000..f849298def --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/index.spec.js @@ -0,0 +1,25 @@ +export default { + props: { + isServer: true, + }, + clientProps: { + isServer: false, + }, + snapshot(target) { + return { + childMarkup: target.shadowRoot.firstChild.firstChild.shadowRoot.innerHTML, + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.childMarkup).not.toBe(snapshots.childMarkup); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration child node mismatch on: - rendered on server:

        - expected on client:
        ,
        ', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/child/child.html new file mode 100644 index 0000000000..1479dba665 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/child/child.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/child/child.js new file mode 100644 index 0000000000..f873c97414 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/child/child.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + @api isServer; + + get things() { + return this.isServer ? ['foo'] : ['foo', 'bar']; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/main/main.html new file mode 100644 index 0000000000..2e3a63e36d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/main/main.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/main/main.js new file mode 100644 index 0000000000..df6ecb77bf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/number-of-child-els/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Parent extends LightningElement { + @api isServer; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/index.spec.js new file mode 100644 index 0000000000..78fdd35939 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).toBe(snapshots.child); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [ + '`validationOptOut` must be `true` or an array of attributes that should not be validated.', + ], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/child/child.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/child/child.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/child/child.js new file mode 100644 index 0000000000..318572f1bb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = [undefined]; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/main/main.html new file mode 100644 index 0000000000..bbabacf0e3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-array-of-non-strings/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/index.spec.js new file mode 100644 index 0000000000..78fdd35939 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).toBe(snapshots.child); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [ + '`validationOptOut` must be `true` or an array of attributes that should not be validated.', + ], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/child/child.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/child/child.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/child/child.js new file mode 100644 index 0000000000..a85dfec430 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = false; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/main/main.html new file mode 100644 index 0000000000..bbabacf0e3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-false/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/index.spec.js new file mode 100644 index 0000000000..78fdd35939 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).toBe(snapshots.child); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [ + '`validationOptOut` must be `true` or an array of attributes that should not be validated.', + ], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/child/child.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/child/child.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/child/child.js new file mode 100644 index 0000000000..30db5fdf7b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = {}; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/main/main.html new file mode 100644 index 0000000000..bbabacf0e3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-non-array/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/index.spec.js new file mode 100644 index 0000000000..78fdd35939 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/index.spec.js @@ -0,0 +1,18 @@ +export default { + snapshot(target) { + return { + child: target.shadowRoot.querySelector('x-child'), + }; + }, + test(target, snapshots, consoleCalls) { + const hydratedSnapshot = this.snapshot(target); + expect(hydratedSnapshot.child).toBe(snapshots.child); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + warn: [ + '`validationOptOut` must be `true` or an array of attributes that should not be validated.', + ], + error: [], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/child/child.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/child/child.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/child/child.js new file mode 100644 index 0000000000..b20b5bfeea --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static validationOptOut = null; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/main/main.html new file mode 100644 index 0000000000..bbabacf0e3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/main/main.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/with-validation-opt-out/warnings/opt-out-null/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/component/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/refs/component/index.spec.js new file mode 100644 index 0000000000..4a2aa80949 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/component/index.spec.js @@ -0,0 +1,11 @@ +export default { + test(target, snapshots, consoleCalls) { + const expected = target.shadowRoot.querySelector('x-child'); + const actual = target.getRef('foo'); + + expect(expected).toBe(actual); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/child/child.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/main/main.html new file mode 100644 index 0000000000..fa61dc4c49 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/main/main.js new file mode 100644 index 0000000000..00de0a7f59 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/component/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api getRef(name) { + return this.refs[name]; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/element/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/refs/element/index.spec.js new file mode 100644 index 0000000000..dfa59de9c1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/element/index.spec.js @@ -0,0 +1,11 @@ +export default { + test(target, snapshots, consoleCalls) { + const expected = target.shadowRoot.querySelector('div'); + const actual = target.getRef('foo'); + + expect(expected).toBe(actual); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/element/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/refs/element/x/main/main.html new file mode 100644 index 0000000000..b383d22f41 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/element/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/refs/element/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/refs/element/x/main/main.js new file mode 100644 index 0000000000..00de0a7f59 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/refs/element/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api getRef(name) { + return this.refs[name]; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/simple/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/simple/index.spec.js new file mode 100644 index 0000000000..3152f00bec --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/simple/index.spec.js @@ -0,0 +1,26 @@ +export default { + props: { + greeting: 'hello!', + }, + snapshot(target) { + const p = target.shadowRoot.querySelector('p'); + return { + p, + text: p.firstChild, + }; + }, + test(target, snapshots) { + const p = target.shadowRoot.querySelector('p'); + expect(p).toBe(snapshots.p); + expect(p.firstChild).toBe(snapshots.text); + expect(p.textContent).toBe('hello!'); + expect(customElements.get(target.tagName.toLowerCase())).not.toBeUndefined(); + expect(customElements.get('x-child')).not.toBeUndefined(); + + target.greeting = 'bye!'; + + return Promise.resolve().then(() => { + expect(p.textContent).toBe('bye!'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/simple/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/simple/x/child/child.html new file mode 100644 index 0000000000..7bc67e3a9b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/simple/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/simple/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/simple/x/child/child.js new file mode 100644 index 0000000000..3f0d7e7e52 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/simple/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/simple/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/simple/x/main/main.html new file mode 100644 index 0000000000..fd34ef2b36 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/simple/x/main/main.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/simple/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/simple/x/main/main.js new file mode 100644 index 0000000000..aa674ccf41 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/simple/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api greeting; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/default/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/slots/default/index.spec.js new file mode 100644 index 0000000000..ee54773506 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/default/index.spec.js @@ -0,0 +1,46 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.shadowRoot.querySelector('x-with-slots'); + const cmpWithSlotParagraphs = cmpWithSlot.shadowRoot.querySelectorAll('p'); + const [mainText, secondText] = cmpWithSlot.querySelectorAll('span'); + + return { + withSlot: cmpWithSlot, + mainText, + secondText, + cmpWithSlotParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.mainText).toBe(snapshots.mainText); + expect(snapshotAfterHydration.secondText).toBe(snapshots.secondText); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + + expect(snapshotAfterHydration.mainText.textContent).toBe('initial'); + + // let's verify handlers + snapshotAfterHydration.mainText.click(); + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + + expect(target.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(1); + + target.slotText = 'changed'; + + return Promise.resolve().then(() => { + const lateSnapshot = this.snapshot(target); + + expect(lateSnapshot.mainText.textContent).toBe('changed'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/main/main.html new file mode 100644 index 0000000000..6162f1c0f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/main/main.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/main/main.js new file mode 100644 index 0000000000..c4aec9cecd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/main/main.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/withSlots/withSlots.html new file mode 100644 index 0000000000..9aba126ee0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/withSlots/withSlots.js new file mode 100644 index 0000000000..d12b871f4a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/default/x/withSlots/withSlots.js @@ -0,0 +1,13 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/named/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/slots/named/index.spec.js new file mode 100644 index 0000000000..ee54773506 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/named/index.spec.js @@ -0,0 +1,46 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.shadowRoot.querySelector('x-with-slots'); + const cmpWithSlotParagraphs = cmpWithSlot.shadowRoot.querySelectorAll('p'); + const [mainText, secondText] = cmpWithSlot.querySelectorAll('span'); + + return { + withSlot: cmpWithSlot, + mainText, + secondText, + cmpWithSlotParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.mainText).toBe(snapshots.mainText); + expect(snapshotAfterHydration.secondText).toBe(snapshots.secondText); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + + expect(snapshotAfterHydration.mainText.textContent).toBe('initial'); + + // let's verify handlers + snapshotAfterHydration.mainText.click(); + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + + expect(target.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(1); + + target.slotText = 'changed'; + + return Promise.resolve().then(() => { + const lateSnapshot = this.snapshot(target); + + expect(lateSnapshot.mainText.textContent).toBe('changed'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/main/main.html new file mode 100644 index 0000000000..0434290c3d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/main/main.html @@ -0,0 +1,10 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/main/main.js new file mode 100644 index 0000000000..c4aec9cecd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/main/main.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/withSlots/withSlots.html new file mode 100644 index 0000000000..19b7e18ca1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/withSlots/withSlots.js new file mode 100644 index 0000000000..d12b871f4a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/named/x/withSlots/withSlots.js @@ -0,0 +1,13 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/index.spec.js new file mode 100644 index 0000000000..06278dffb0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/index.spec.js @@ -0,0 +1,54 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.shadowRoot.querySelector('x-with-slots'); + const cmpChild = target.shadowRoot.querySelector('x-child'); + const cmpWithSlotParagraphs = cmpWithSlot.shadowRoot.querySelectorAll('p'); + const childParagraphs = cmpChild.shadowRoot.querySelectorAll('p'); + + const [mainText, secondText] = cmpWithSlot.querySelectorAll('span'); + + return { + withSlot: cmpWithSlot, + cmpChild, + mainText, + secondText, + cmpWithSlotParagraphs, + childParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.mainText).toBe(snapshots.mainText); + expect(snapshotAfterHydration.secondText).toBe(snapshots.secondText); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + expect(snapshotAfterHydration.childParagraphs).toEqual(snapshots.childParagraphs); + + expect(snapshotAfterHydration.mainText.textContent).toBe('initial'); + + // let's verify handlers + snapshotAfterHydration.mainText.click(); + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + snapshotAfterHydration.childParagraphs[0].click(); + + expect(target.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(1); + expect(snapshotAfterHydration.cmpChild.timesHandlerIsExecuted).toBe(1); + + target.slotText = 'changed'; + + return Promise.resolve().then(() => { + const lateSnapshot = this.snapshot(target); + + expect(lateSnapshot.mainText.textContent).toBe('changed'); + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/child/child.html new file mode 100644 index 0000000000..9aba126ee0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/child/child.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/child/child.js new file mode 100644 index 0000000000..10342073dc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/child/child.js @@ -0,0 +1,13 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/main/main.html new file mode 100644 index 0000000000..5493886ebb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/main/main.html @@ -0,0 +1,10 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/main/main.js new file mode 100644 index 0000000000..c4aec9cecd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/main/main.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/withSlots/withSlots.html new file mode 100644 index 0000000000..9aba126ee0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/withSlots/withSlots.js new file mode 100644 index 0000000000..d12b871f4a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/nested/x/withSlots/withSlots.js @@ -0,0 +1,13 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/index.spec.js new file mode 100644 index 0000000000..510a2b8e20 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/index.spec.js @@ -0,0 +1,31 @@ +// NOTE: Disconnected callback is not triggered by Node.remove, see: https://github.com/salesforce/lwc/issues/1102 +// That's why we trick it by removing a component via the diffing algo. +export default { + props: { + slotText: 'initial', + }, + snapshot(target) { + const cmpWithSlot = target.shadowRoot.querySelector('x-with-slots'); + const cmpWithSlotParagraphs = cmpWithSlot.shadowRoot.querySelectorAll('p'); + + return { + withSlot: cmpWithSlot, + cmpWithSlotParagraphs, + }; + }, + test(target, snapshots) { + const snapshotAfterHydration = this.snapshot(target); + + expect(snapshotAfterHydration.withSlot).toBe(snapshots.withSlot); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toHaveSize(3); + expect(snapshotAfterHydration.cmpWithSlotParagraphs).toEqual( + snapshots.cmpWithSlotParagraphs + ); + + // let's verify handlers + snapshotAfterHydration.cmpWithSlotParagraphs[0].click(); + snapshotAfterHydration.cmpWithSlotParagraphs[1].click(); + + expect(snapshotAfterHydration.withSlot.timesHandlerIsExecuted).toBe(2); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/main/main.html new file mode 100644 index 0000000000..956bf1c67f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/main/main.js new file mode 100644 index 0000000000..c4aec9cecd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/main/main.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + _executedHandlerCounter = 0; + + @api slotText; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/withSlots/withSlots.html b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/withSlots/withSlots.html new file mode 100644 index 0000000000..d29301f03c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/withSlots/withSlots.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/withSlots/withSlots.js b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/withSlots/withSlots.js new file mode 100644 index 0000000000..d12b871f4a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/slots/no-content/x/withSlots/withSlots.js @@ -0,0 +1,13 @@ +import { LightningElement, api } from 'lwc'; + +export default class WithSlots extends LightningElement { + _executedHandlerCounter = 0; + @api + get timesHandlerIsExecuted() { + return this._executedHandlerCounter; + } + + handleClick() { + this._executedHandlerCounter++; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/index.spec.js new file mode 100644 index 0000000000..a47250b831 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/index.spec.js @@ -0,0 +1,10 @@ +export default { + test(elm) { + // should apply style to the host element + expect(window.getComputedStyle(elm).marginLeft).toBe('10px'); + + // should apply style to the host element with the matching attributes + elm.setAttribute('data-styled', true); + expect(window.getComputedStyle(elm).marginLeft).toBe('20px'); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.css b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.css new file mode 100644 index 0000000000..6e17044950 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.css @@ -0,0 +1,8 @@ +:host { + display: block; + margin-left: 10px; +} + +:host([data-styled]) { + margin-left: 20px; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.html new file mode 100644 index 0000000000..f8c88139b8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/apply-style-to-host/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/index.spec.js new file mode 100644 index 0000000000..9a3c29b1b6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/index.spec.js @@ -0,0 +1,18 @@ +export default { + test(target, snapshots, consoleCalls) { + const h1 = target.shadowRoot.querySelector('h1'); + const h2 = target.shadowRoot.querySelector('h2'); + const div = target.shadowRoot.querySelector('div'); + + expect(getComputedStyle(h1).color).toEqual('rgb(255, 0, 0)'); + expect(getComputedStyle(h1).backgroundColor).toEqual('rgb(0, 0, 255)'); + + expect(getComputedStyle(h2).color).toEqual('rgb(0, 128, 0)'); + + expect(getComputedStyle(div).color).toEqual('rgb(128, 0, 128)'); + expect(getComputedStyle(div).backgroundColor).toEqual('rgb(255, 255, 0)'); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.css b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.css new file mode 100644 index 0000000000..404ed144af --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.css @@ -0,0 +1,24 @@ +/* single quote */ +h1[data-foo='bar'] { + color: red; +} + +/* double quote */ +h1[data-foo='bar'] { + background-color: blue; +} + +/* less-than */ +h2[data-foo='<'] { + color: green; +} + +/* greater-than */ +h2 > div { + background-color: yellow; +} + +/* ampersand */ +div[data-foo='&'] { + color: purple; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.html new file mode 100644 index 0000000000..4ac0193774 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.html @@ -0,0 +1,6 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.js new file mode 100644 index 0000000000..80164ac007 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/escaping/x/main/main.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Main extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/index.spec.js new file mode 100644 index 0000000000..087a1cdf0b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/index.spec.js @@ -0,0 +1,21 @@ +export default { + snapshot(target) { + const child = target.querySelector('x-child'); + return { + child, + classList: new Set([...child.classList]), + h1: target.querySelector('h1'), + }; + }, + test(target, snapshots, consoleCalls) { + const child = target.querySelector('x-child'); + const h1 = target.querySelector('h1'); + expect(child).toBe(snapshots.child); + expect(h1).toBe(snapshots.h1); + + expect(new Set([...child.classList])).toEqual(snapshots.classList); + + expect(consoleCalls.warn).toHaveSize(0); + expect(consoleCalls.error).toHaveSize(0); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.html new file mode 100644 index 0000000000..ff253f9e05 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.scoped.css new file mode 100644 index 0000000000..e67c527284 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/child/child.scoped.css @@ -0,0 +1,3 @@ +h1 { + color: darkmagenta; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/main/main.html new file mode 100644 index 0000000000..586e14e39f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/main/main.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/basic/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/index.spec.js new file mode 100644 index 0000000000..f716daa1a0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/index.spec.js @@ -0,0 +1,29 @@ +export default { + props: { + clazz: '', + }, + clientProps: { + clazz: 'foo', + }, + snapshot(target) { + const child = target.shadowRoot.querySelector('x-child'); + return { + child, + h1: child.shadowRoot.querySelector('h1'), + }; + }, + test(target, snapshots, consoleCalls) { + const child = target.shadowRoot.querySelector('x-child'); + const h1 = child.shadowRoot.querySelector('h1'); + expect(child).not.toBe(snapshots.child); + expect(h1).not.toBe(snapshots.h1); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: class="" - expected on client: class="foo"', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/child.js new file mode 100644 index 0000000000..75e9403ad7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/child.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; +import tmpl from './tmpl.html'; + +export default class Child extends LightningElement { + render() { + return tmpl; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/tmpl.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/tmpl.html new file mode 100644 index 0000000000..5ff0df9d16 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/tmpl.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/tmpl.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/tmpl.scoped.css new file mode 100644 index 0000000000..a26bc6195f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/child/tmpl.scoped.css @@ -0,0 +1,3 @@ +h1 { + color: sienna; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/main/main.html new file mode 100644 index 0000000000..1ff8264f43 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/main/main.js new file mode 100644 index 0000000000..c78615a5a3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-client/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api clazz; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/index.spec.js new file mode 100644 index 0000000000..2a3a0fdc62 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/index.spec.js @@ -0,0 +1,29 @@ +export default { + props: { + clazz: 'foo', + }, + clientProps: { + clazz: '', + }, + snapshot(target) { + const child = target.shadowRoot.querySelector('x-child'); + return { + child, + h1: child.shadowRoot.querySelector('h1'), + }; + }, + test(target, snapshots, consoleCalls) { + const child = target.shadowRoot.querySelector('x-child'); + const h1 = child.shadowRoot.querySelector('h1'); + expect(child).not.toBe(snapshots.child); + expect(h1).not.toBe(snapshots.h1); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: - rendered on server: class="foo" - expected on client: class=""', + 'Hydration completed with errors.', + ], + }); + }, +}; diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/child.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/child.js new file mode 100644 index 0000000000..75e9403ad7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/child.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; +import tmpl from './tmpl.html'; + +export default class Child extends LightningElement { + render() { + return tmpl; + } +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/tmpl.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/tmpl.html new file mode 100644 index 0000000000..5ff0df9d16 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/tmpl.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/tmpl.scoped.css b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/tmpl.scoped.css new file mode 100644 index 0000000000..a26bc6195f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/child/tmpl.scoped.css @@ -0,0 +1,3 @@ +h1 { + color: sienna; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/main/main.html b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/main/main.html new file mode 100644 index 0000000000..1ff8264f43 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/main/main.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/main/main.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/main/main.js new file mode 100644 index 0000000000..c78615a5a3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/extra-class-in-server/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Main extends LightningElement { + @api clazz; +} diff --git a/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/wrong-scoped-template/index.spec.js b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/wrong-scoped-template/index.spec.js new file mode 100644 index 0000000000..648071b0ac --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/stylesheet/host-scope-token/dynamic-render/mismatches/wrong-scoped-template/index.spec.js @@ -0,0 +1,37 @@ +export default { + props: { + showA: false, + }, + clientProps: { + showA: true, + }, + snapshot(target) { + const child = target.shadowRoot.querySelector('x-child'); + return { + child, + classList: new Set([...child.classList]), + h1: child.shadowRoot.querySelector('h1'), + }; + }, + test(target, snapshots, consoleCalls) { + const child = target.shadowRoot.querySelector('x-child'); + const h1 = child.shadowRoot.querySelector('h1'); + + // is not considered mismatched but its children are + expect(child).toBe(snapshots.child); + expect(h1).not.toBe(snapshots.h1); + + expect( + getComputedStyle(child).getPropertyValue('--from-template').trim().replace(/"/g, "'") + ).toBe("'a'"); + + TestUtils.expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration attribute mismatch on: +
        + `; + + document.body.appendChild(div); + + const supports = + getComputedStyle(div.querySelector('.test-dir-pseudo')).color === 'rgb(255, 0, 0)'; + document.body.removeChild(div); + return supports; +} + +// In native shadow we delegate to the browser, so it has to support :dir() +describe.runIf(!process.env.NATIVE_SHADOW || supportsDirPseudoclass())(':dir() pseudoclass', () => { + it('can apply styles based on :dir()', () => { + const elm = createElement('x-parent', { is: Component }); + document.body.appendChild(elm); + + elm.setAttribute('dir', 'ltr'); + + return Promise.resolve() + .then(() => { + expect(getComputedStyle(elm.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(0, 0, 1)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.foo')).color).toEqual( + 'rgb(0, 0, 3)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.foo.bar')).color).toEqual( + 'rgb(0, 0, 5)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.baz span')).color).toEqual( + 'rgb(0, 0, 7)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.baz button')).color).toEqual( + 'rgb(0, 0, 9)' + ); + elm.setAttribute('dir', 'rtl'); + }) + .then(() => { + expect(getComputedStyle(elm.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(0, 0, 2)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.foo')).color).toEqual( + 'rgb(0, 0, 4)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.foo.bar')).color).toEqual( + 'rgb(0, 0, 6)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.baz span')).color).toEqual( + 'rgb(0, 0, 8)' + ); + expect(getComputedStyle(elm.shadowRoot.querySelector('.baz button')).color).toEqual( + 'rgb(0, 0, 10)' + ); + }); + }); + + it('can apply styles based on :dir() for light-within-shadow', () => { + const elm = createElement('x-shadow-container', { is: ShadowContainer }); + document.body.appendChild(elm); + + elm.setAttribute('dir', 'ltr'); + + return Promise.resolve() + .then(() => { + expect(getComputedStyle(elm.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(0, 0, 1)' + ); + elm.setAttribute('dir', 'rtl'); + }) + .then(() => { + expect(getComputedStyle(elm.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(0, 0, 2)' + ); + }); + }); +}); + +it.runIf(process.env.NATIVE_SHADOW && supportsDirPseudoclass())( + 'can apply styles based on :dir() for light-at-root', + () => { + const elm = createElement('x-light', { is: Light }); + document.body.appendChild(elm); + + return Promise.resolve() + .then(() => { + // Unlike [dir], :dir(ltr) matches even when there is no dir attribute anywhere + expect(getComputedStyle(elm.querySelector('div')).color).toEqual('rgb(0, 0, 1)'); + elm.setAttribute('dir', 'rtl'); + }) + .then(() => { + expect(getComputedStyle(elm.querySelector('div')).color).toEqual('rgb(0, 0, 2)'); + elm.setAttribute('dir', 'ltr'); + }) + .then(() => { + expect(getComputedStyle(elm.querySelector('div')).color).toEqual('rgb(0, 0, 1)'); + }); + } +); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.css b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.css new file mode 100644 index 0000000000..6128229f3a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.css @@ -0,0 +1,39 @@ +div:dir(ltr) { + color: #000001; +} + +div:dir(rtl) { + color: #000002; +} + +.foo:dir(ltr):not(.bar) { + color: #000003; +} + +.foo:dir(rtl):not(.bar) { + color: #000004; +} + +.foo:dir(ltr) { + color: #000005; +} + +.foo:dir(rtl) { + color: #000006; +} + +.baz:dir(ltr) span { + color: #000007; +} + +.baz:dir(rtl) span { + color: #000008; +} + +.baz button:dir(ltr) { + color: #000009; +} + +.baz button:dir(rtl) { + color: #00000a; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.html b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.html new file mode 100644 index 0000000000..e0bbe7a3ca --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.html @@ -0,0 +1,9 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.js b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.js new file mode 100644 index 0000000000..6d3542bb2f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Component extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.css b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.css new file mode 100644 index 0000000000..8d29fcff15 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.css @@ -0,0 +1,7 @@ +div:dir(ltr) { + color: #000001; +} + +div:dir(rtl) { + color: #000002; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.html b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.html new file mode 100644 index 0000000000..78dc99f8d0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.js b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.js new file mode 100644 index 0000000000..1152396f29 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/light/light.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Light extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/shadowContainer/shadowContainer.html b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/shadowContainer/shadowContainer.html new file mode 100644 index 0000000000..33f955c6e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/shadowContainer/shadowContainer.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/shadowContainer/shadowContainer.js b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/shadowContainer/shadowContainer.js new file mode 100644 index 0000000000..fbb5fb517c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/dir-pseudoclass/x/shadowContainer/shadowContainer.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class ShadowContainer extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/event-post-dispatch.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/event-post-dispatch.spec.js new file mode 100644 index 0000000000..649afaae4d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/event-post-dispatch.spec.js @@ -0,0 +1,88 @@ +// Inspired from WPT: +// https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-post-dispatch.html + +import { createElement } from 'lwc'; +import { extractDataIds } from 'test-utils'; + +import Container from 'x/container'; + +function assertEventStateReset(evt) { + expect(evt.eventPhase).toBe(0); + expect(evt.currentTarget).toBe(null); + expect(evt.composedPath().length).toBe(0); +} + +function createComponent() { + const element = createElement('x-container', { is: Container }); + element.setAttribute('data-id', 'x-container'); + document.body.appendChild(element); + return extractDataIds(element); +} + +describe('post-dispatch event state', () => { + describe('native element', () => { + it('{ bubbles: true, composed: true }', () => { + const nodes = createComponent(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + nodes.container_div.dispatchEvent(event); + + assertEventStateReset(event); + expect(event.target).toBe(nodes['x-container']); + }); + + it('{ bubbles: true, composed: false }', () => { + const nodes = createComponent(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + nodes.container_div.dispatchEvent(event); + + assertEventStateReset(event); + expect(event.target).toBe(null); + }); + }); + + describe('lwc:dom="manual" element', () => { + it('{ bubbles: true, composed: true }', () => { + const nodes = createComponent(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + nodes.container_span_manual.dispatchEvent(event); + + // lwc:dom=manual is async due to MutationObserver + return new Promise(setTimeout).then(() => { + assertEventStateReset(event); + expect(event.target).toBe(nodes['x-container']); + }); + }); + + it('{ bubbles: true, composed: false }', () => { + const nodes = createComponent(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + nodes.container_span_manual.dispatchEvent(event); + + // lwc:dom=manual is async due to MutationObserver + return new Promise(setTimeout).then(() => { + assertEventStateReset(event); + expect(event.target).toBe(null); + }); + }); + }); + + describe('component', () => { + it('{ bubbles: true, composed: true }', () => { + const nodes = createComponent(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + nodes['x-container'].dispatchEventComponent(event); + + assertEventStateReset(event); + expect(event.target).toBe(nodes['x-container']); + }); + + it('{ bubbles: true, composed: false }', () => { + const nodes = createComponent(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + nodes['x-container'].dispatchEventComponent(event); + + assertEventStateReset(event); + expect(event.target).toBe(nodes['x-container']); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/propagation.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/propagation.spec.js new file mode 100644 index 0000000000..ba2920bab0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/propagation.spec.js @@ -0,0 +1,630 @@ +// Inspired from WPT: +// https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-inside-shadow-tree.html + +import { createElement } from 'lwc'; +import { extractDataIds } from 'test-utils'; + +import Container from 'x/container'; + +function dispatchEventWithLog(target, nodes, event) { + const log = []; + + [...Object.values(nodes), document.body, document.documentElement, document, window].forEach( + (node) => { + node.addEventListener(event.type, (event) => { + log.push([node, event.target, event.composedPath()]); + }); + } + ); + + target.dispatchEvent(event); + return log; +} + +function createTestElement() { + const elm = createElement('x-container', { is: Container }); + elm.setAttribute('data-id', 'x-container'); + document.body.appendChild(elm); + return extractDataIds(elm); +} + +function createDisconnectedTestElement() { + const fragment = document.createDocumentFragment(); + const elm = createElement('x-container', { is: Container }); + elm.setAttribute('data-id', 'x-container'); + + const doAppend = () => fragment.appendChild(elm); + + if (!lwcRuntimeFlags.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) { + doAppend(); + } else { + // Expected warning, since we are working with disconnected nodes + expect(doAppend).toLogWarningDev( + Array(4) + .fill() + .map( + () => + /fired a `connectedCallback` and rendered, but was not connected to the DOM/ + ) + ); + } + + const nodes = extractDataIds(elm); + // Manually added because document fragments can't have attributes. + nodes.fragment = fragment; + return nodes; +} + +describe('event propagation', () => { + describe('dispatched on native element', () => { + it('{bubbles: true, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + const actualLogs = dispatchEventWithLog(nodes.button, nodes, event); + + const composedPath = [ + nodes.button, + nodes.button_div, + nodes['x-button'].shadowRoot, + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + const expectedLogs = [ + [nodes.button, nodes.button, composedPath], + [nodes.button_div, nodes.button, composedPath], + [nodes['x-button'].shadowRoot, nodes.button, composedPath], + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes.button_group_slot, nodes['x-button'], composedPath], + [nodes.button_group_internal_slot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'], nodes['x-button'], composedPath], + [nodes.button_group_div, nodes['x-button'], composedPath], + [nodes['x-button-group'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group'], nodes['x-button'], composedPath], + [nodes.container_div, nodes['x-button'], composedPath], + [nodes['x-container'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + [document.body, nodes['x-container'], composedPath], + [document.documentElement, nodes['x-container'], composedPath], + [document, nodes['x-container'], composedPath], + [window, nodes['x-container'], composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: true }); + const actualLogs = dispatchEventWithLog(nodes.button, nodes, event); + + const composedPath = [ + nodes.button, + nodes.button_div, + nodes['x-button'].shadowRoot, + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + + let expectedLogs; + if (process.env.NATIVE_SHADOW) { + expectedLogs = [ + [nodes.button, nodes.button, composedPath], + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + ]; + } else { + // TODO [#1138]: {bubbles: false, composed: true} events should invoke event listeners on ancestor hosts + expectedLogs = [[nodes.button, nodes.button, composedPath]]; + } + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: true, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + const actualLogs = dispatchEventWithLog(nodes.button, nodes, event); + + const composedPath = [nodes.button, nodes.button_div, nodes['x-button'].shadowRoot]; + + const expectedLogs = [ + [nodes.button, nodes.button, composedPath], + [nodes.button_div, nodes.button, composedPath], + [nodes['x-button'].shadowRoot, nodes.button, composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: false }); + const actualLogs = dispatchEventWithLog(nodes.button, nodes, event); + + const composedPath = [nodes.button, nodes.button_div, nodes['x-button'].shadowRoot]; + const expectedLogs = [[nodes.button, nodes.button, composedPath]]; + + expect(actualLogs).toEqual(expectedLogs); + }); + }); + + describe('dispatched on host element', () => { + it('{bubbles: true, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + const actualLogs = dispatchEventWithLog(nodes['x-button'], nodes, event); + + const composedPath = [ + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + const expectedLogs = [ + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes.button_group_slot, nodes['x-button'], composedPath], + [nodes.button_group_internal_slot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'], nodes['x-button'], composedPath], + [nodes.button_group_div, nodes['x-button'], composedPath], + [nodes['x-button-group'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group'], nodes['x-button'], composedPath], + [nodes.container_div, nodes['x-button'], composedPath], + [nodes['x-container'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + [document.body, nodes['x-container'], composedPath], + [document.documentElement, nodes['x-container'], composedPath], + [document, nodes['x-container'], composedPath], + [window, nodes['x-container'], composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: true, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + const actualLogs = dispatchEventWithLog(nodes['x-button'], nodes, event); + + const composedPath = [ + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + ]; + + const expectedLogs = [ + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes.button_group_slot, nodes['x-button'], composedPath], + [nodes.button_group_internal_slot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'], nodes['x-button'], composedPath], + [nodes.button_group_div, nodes['x-button'], composedPath], + [nodes['x-button-group'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group'], nodes['x-button'], composedPath], + [nodes.container_div, nodes['x-button'], composedPath], + [nodes['x-container'].shadowRoot, nodes['x-button'], composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: true }); + const actualLogs = dispatchEventWithLog(nodes['x-button'], nodes, event); + + const composedPath = [ + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + + let expectedLogs; + if (process.env.NATIVE_SHADOW) { + expectedLogs = [ + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + ]; + } else { + expectedLogs = [[nodes['x-button'], nodes['x-button'], composedPath]]; + } + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: false }); + const actualLogs = dispatchEventWithLog(nodes['x-button'], nodes, event); + + const composedPath = [ + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + ]; + const expectedLogs = [[nodes['x-button'], nodes['x-button'], composedPath]]; + + expect(actualLogs).toEqual(expectedLogs); + }); + }); + + describe('dispatched on shadow root', () => { + it('{bubbles: true, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + const actualLogs = dispatchEventWithLog(nodes['x-button'].shadowRoot, nodes, event); + + const composedPath = [ + nodes['x-button'].shadowRoot, + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + const expectedLogs = [ + [nodes['x-button'].shadowRoot, nodes['x-button'].shadowRoot, composedPath], + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes.button_group_slot, nodes['x-button'], composedPath], + [nodes.button_group_internal_slot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group-internal'], nodes['x-button'], composedPath], + [nodes.button_group_div, nodes['x-button'], composedPath], + [nodes['x-button-group'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-button-group'], nodes['x-button'], composedPath], + [nodes.container_div, nodes['x-button'], composedPath], + [nodes['x-container'].shadowRoot, nodes['x-button'], composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + [document.body, nodes['x-container'], composedPath], + [document.documentElement, nodes['x-container'], composedPath], + [document, nodes['x-container'], composedPath], + [window, nodes['x-container'], composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: true }); + const actualLogs = dispatchEventWithLog(nodes['x-button'].shadowRoot, nodes, event); + + const composedPath = [ + nodes['x-button'].shadowRoot, + nodes['x-button'], + nodes.button_group_slot, + nodes.button_group_internal_slot, + nodes['x-button-group-internal'].shadowRoot, + nodes['x-button-group-internal'], + nodes.button_group_div, + nodes['x-button-group'].shadowRoot, + nodes['x-button-group'], + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + + let expectedLogs; + if (process.env.NATIVE_SHADOW) { + expectedLogs = [ + [nodes['x-button'].shadowRoot, nodes['x-button'].shadowRoot, composedPath], + [nodes['x-button'], nodes['x-button'], composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + ]; + } else { + expectedLogs = [ + [nodes['x-button'].shadowRoot, nodes['x-button'].shadowRoot, composedPath], + [nodes['x-button'], nodes['x-button'], composedPath], + ]; + } + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: true, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + const actualLogs = dispatchEventWithLog(nodes['x-button'].shadowRoot, nodes, event); + + const composedPath = [nodes['x-button'].shadowRoot]; + const expectedLogs = [ + [nodes['x-button'].shadowRoot, nodes['x-button'].shadowRoot, composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: false }); + const actualLogs = dispatchEventWithLog(nodes['x-button'].shadowRoot, nodes, event); + + const composedPath = [nodes['x-button'].shadowRoot]; + const expectedLogs = [ + [nodes['x-button'].shadowRoot, nodes['x-button'].shadowRoot, composedPath], + ]; + + expect(actualLogs).toEqual(expectedLogs); + }); + }); + + describe('dispatched on lwc:dom="manual" node', () => { + it('{bubbles: true, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + + const composedPath = [ + nodes.container_span_manual, + nodes.container_span, + nodes['x-container'].shadowRoot, + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + const expectedLogs = [ + [nodes.container_span_manual, nodes.container_span_manual, composedPath], + [nodes.container_span, nodes.container_span_manual, composedPath], + [nodes['x-container'].shadowRoot, nodes.container_span_manual, composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + [document.body, nodes['x-container'], composedPath], + [document.documentElement, nodes['x-container'], composedPath], + [document, nodes['x-container'], composedPath], + [window, nodes['x-container'], composedPath], + ]; + + return new Promise(setTimeout).then(() => { + const actualLogs = dispatchEventWithLog(nodes.container_span_manual, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + }); + + it('{bubbles: true, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + + const composedPath = [ + nodes.container_span_manual, + nodes.container_span, + nodes['x-container.shadowRoot'], + ]; + const expectedLogs = [ + [nodes.container_span_manual, nodes.container_span_manual, composedPath], + [nodes.container_span, nodes.container_span_manual, composedPath], + [nodes['x-container'].shadowRoot, nodes.container_span_manual, composedPath], + ]; + + return Promise.resolve().then(() => { + const actualLogs = dispatchEventWithLog(nodes.container_span_manual, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + }); + + it('{bubbles: false, composed: true}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: true }); + + const composedPath = [ + nodes.container_span_manual, + nodes.container_span, + nodes['x-container.shadowRoot'], + nodes['x-container'], + document.body, + document.documentElement, + document, + window, + ]; + + let expectedLogs; + if (process.env.NATIVE_SHADOW) { + expectedLogs = [ + [nodes.container_span_manual, nodes.container_span_manual, composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + ]; + } else { + expectedLogs = [ + [nodes.container_span_manual, nodes.container_span_manual, composedPath], + ]; + } + + return Promise.resolve().then(() => { + const actualLogs = dispatchEventWithLog(nodes.container_span_manual, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + }); + + it('{bubbles: false, composed: false}', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: false }); + + const composedPath = [ + nodes.container_span_manual, + nodes.container_span, + nodes['x-container.shadowRoot'], + ]; + const expectedLogs = [ + [nodes.container_span_manual, nodes.container_span_manual, composedPath], + ]; + + return Promise.resolve().then(() => { + const actualLogs = dispatchEventWithLog(nodes.container_span_manual, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + }); + }); + + // This test does not work with native custom element lifecycle because disconnected + // fragments cannot fire connectedCallback/disconnectedCallback events + describe.runIf(lwcRuntimeFlags.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE)( + 'dispatched within a disconnected tree', + () => { + it('{bubbles: true, composed: true}', () => { + const nodes = createDisconnectedTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + + const composedPath = [ + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + nodes.fragment, + ]; + const expectedLogs = [ + [nodes.container_div, nodes.container_div, composedPath], + [nodes['x-container'].shadowRoot, nodes.container_div, composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + [nodes.fragment, nodes['x-container'], composedPath], + ]; + + const actualLogs = dispatchEventWithLog(nodes.container_div, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: true, composed: false}', () => { + const nodes = createDisconnectedTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: false }); + + const composedPath = [nodes.container_div, nodes['x-container'].shadowRoot]; + const expectedLogs = [ + [nodes.container_div, nodes.container_div, composedPath], + [nodes['x-container'].shadowRoot, nodes.container_div, composedPath], + ]; + + const actualLogs = dispatchEventWithLog(nodes.container_div, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: true}', () => { + const nodes = createDisconnectedTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: true }); + + const composedPath = [ + nodes.container_div, + nodes['x-container'].shadowRoot, + nodes['x-container'], + nodes.fragment, + ]; + + let expectedLogs; + if (process.env.NATIVE_SHADOW) { + expectedLogs = [ + [nodes.container_div, nodes.container_div, composedPath], + [nodes['x-container'], nodes['x-container'], composedPath], + ]; + } else { + expectedLogs = [[nodes.container_div, nodes.container_div, composedPath]]; + } + + const actualLogs = dispatchEventWithLog(nodes.container_div, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + + it('{bubbles: false, composed: false}', () => { + const nodes = createDisconnectedTestElement(); + const event = new CustomEvent('test', { bubbles: false, composed: false }); + + const composedPath = [nodes.container_div, nodes['x-container'].shadowRoot]; + const expectedLogs = [[nodes.container_div, nodes.container_div, composedPath]]; + + const actualLogs = dispatchEventWithLog(nodes.container_div, nodes, event); + expect(actualLogs).toEqual(expectedLogs); + }); + } + ); +}); + +describe('declarative event listener', () => { + it('when dispatching instance of Event', () => { + const nodes = createTestElement(); + const event = new Event('test', { bubbles: true, composed: true }); + nodes.button.dispatchEvent(event); + + expect(nodes['x-container'].testEventReceived).toBeTrue(); + }); + + it('when dispatching instance of CustomEvent', () => { + const nodes = createTestElement(); + const event = new CustomEvent('test', { bubbles: true, composed: true }); + nodes.button.dispatchEvent(event); + + expect(nodes['x-container'].testEventReceived).toBeTrue(); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/button/button.html b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/button/button.html new file mode 100644 index 0000000000..7c4c4475e6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/button/button.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/button/button.js b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/button/button.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/button/button.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroup/buttonGroup.html b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroup/buttonGroup.html new file mode 100644 index 0000000000..eefb05113e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroup/buttonGroup.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroup/buttonGroup.js b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroup/buttonGroup.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroup/buttonGroup.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroupInternal/buttonGroupInternal.html b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroupInternal/buttonGroupInternal.html new file mode 100644 index 0000000000..da292f7b44 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroupInternal/buttonGroupInternal.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroupInternal/buttonGroupInternal.js b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroupInternal/buttonGroupInternal.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/buttonGroupInternal/buttonGroupInternal.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/container/container.html b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/container/container.html new file mode 100644 index 0000000000..5cc8426b4b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/container/container.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/container/container.js b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/container/container.js new file mode 100644 index 0000000000..81f9ada2ef --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/x/container/container.js @@ -0,0 +1,24 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + dispatchEventComponent(event) { + this.dispatchEvent(event); + } + + @api + get testEventReceived() { + return this._testEventReceived || false; + } + + handleTest() { + this._testEventReceived = true; + } + + renderedCallback() { + const spanManual = document.createElement('span'); + spanManual.setAttribute('data-id', 'container_span_manual'); + const span = this.template.querySelector('[data-id="container_span"]'); + span.appendChild(spanManual); + } +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/issue-1090.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/issue-1090.spec.js new file mode 100644 index 0000000000..ca82ae97cd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/issue-1090.spec.js @@ -0,0 +1,13 @@ +import { createElement } from 'lwc'; + +import Parent from 'x/parent'; + +describe('Issue #1090', () => { + it('should disconnect slotted content even if it is not allocated into a slot', () => { + const elm = createElement('x-parent', { is: Parent }); + document.body.appendChild(elm); + expect(() => { + document.body.removeChild(elm); + }).not.toThrow(); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/child/child.html b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/child/child.html new file mode 100644 index 0000000000..d8a9fafacf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/child/child.js b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/child/child.js new file mode 100644 index 0000000000..21a45d50a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Container extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/parent/parent.html b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/parent/parent.html new file mode 100644 index 0000000000..a98cacc1d5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/parent/parent.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/parent/parent.js b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/parent/parent.js new file mode 100644 index 0000000000..21a45d50a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/parent/parent.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Container extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/slotted/slotted.html b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/slotted/slotted.html new file mode 100644 index 0000000000..fd35a22577 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/slotted/slotted.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/slotted/slotted.js b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/slotted/slotted.js new file mode 100644 index 0000000000..21a45d50a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/ignoring-slotted/x/slotted/slotted.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Container extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.css b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.css new file mode 100644 index 0000000000..dda2cdf86b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.css @@ -0,0 +1,3 @@ +h1 { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.html b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.html new file mode 100644 index 0000000000..872866ee88 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.js b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/@x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/index.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/index.spec.js new file mode 100644 index 0000000000..bfbc04f0f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/index.spec.js @@ -0,0 +1,31 @@ +import { createElement } from 'lwc'; +import ComponentAtX from '@x/component'; +import ComponentXHashY from 'x#y/component'; + +describe('invalid character @ in namespace', () => { + let elm; + beforeEach(() => { + elm = createElement('x-component', { is: ComponentAtX }); + document.body.appendChild(elm); + }); + + it('element renders despite invalid char in namespace', () => { + const h1 = elm.shadowRoot.querySelector('h1'); + expect(h1.textContent).toEqual('Hello world'); + expect(getComputedStyle(h1).color).toEqual('rgb(0, 128, 0)'); + }); +}); + +describe('invalid character # in namespace', () => { + let elm; + beforeEach(() => { + elm = createElement('xy-component', { is: ComponentXHashY }); + document.body.appendChild(elm); + }); + + it('element renders despite invalid char in namespace', () => { + const h1 = elm.shadowRoot.querySelector('h1'); + expect(h1.textContent).toEqual('Hello world'); + expect(getComputedStyle(h1).color).toEqual('rgb(0, 128, 0)'); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.css b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.css new file mode 100644 index 0000000000..dda2cdf86b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.css @@ -0,0 +1,3 @@ +h1 { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.html b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.html new file mode 100644 index 0000000000..872866ee88 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.js b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/invalid-char-in-namespace/x#y/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/index.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/index.spec.js new file mode 100644 index 0000000000..84170e115c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/index.spec.js @@ -0,0 +1,105 @@ +import { createElement } from 'lwc'; +import Multi from 'x/multi'; +import MultiNoStyleInFirst from 'x/multiNoStyleInFirst'; + +describe.runIf(process.env.NATIVE_SHADOW)( + 'Shadow DOM styling - multiple shadow DOM components', + () => { + it('Does not duplicate styles if template is re-rendered', () => { + const element = createElement('x-multi', { is: Multi }); + + const getNumStyleSheets = () => { + let count = 0; + if (element.shadowRoot.adoptedStyleSheets) { + count += element.shadowRoot.adoptedStyleSheets.length; + } + count += element.shadowRoot.querySelectorAll('style').length; + return count; + }; + + document.body.appendChild(element); + return Promise.resolve() + .then(() => { + expect(getComputedStyle(element.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(0, 0, 255)' + ); + expect(getNumStyleSheets()).toEqual(1); + element.next(); + }) + .then(() => { + expect(getComputedStyle(element.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(255, 0, 0)' + ); + expect(getNumStyleSheets()).toEqual(2); + element.next(); + }) + .then(() => { + expect(getComputedStyle(element.shadowRoot.querySelector('div')).color).toEqual( + 'rgb(0, 0, 255)' + ); + expect(getNumStyleSheets()).toEqual(2); + }); + }); + } +); + +describe('multiple stylesheets rendered in same component', () => { + it('works when first template has no style but second template does', () => { + const element = createElement('x-multi-no-style-in-first', { is: MultiNoStyleInFirst }); + document.body.appendChild(element); + return Promise.resolve() + .then(() => { + expect(getComputedStyle(element.shadowRoot.querySelector('.red')).color).toEqual( + 'rgb(0, 0, 0)' + ); + expect(getComputedStyle(element.shadowRoot.querySelector('.green')).color).toEqual( + 'rgb(0, 0, 0)' + ); + expect(getComputedStyle(element).marginLeft).toEqual('0px'); + element.next(); + return new Promise((resolve) => requestAnimationFrame(() => resolve())); + }) + .then(() => { + expect(getComputedStyle(element.shadowRoot.querySelector('.red')).color).toEqual( + 'rgb(255, 0, 0)' + ); + expect(getComputedStyle(element.shadowRoot.querySelector('.green')).color).toEqual( + 'rgb(0, 128, 0)' + ); + expect(getComputedStyle(element).marginLeft).toEqual('5px'); + element.next(); + return new Promise((resolve) => requestAnimationFrame(() => resolve())); + }) + .then(() => { + if (process.env.NATIVE_SHADOW) { + // TODO [#2466]: In native shadow, stylesheets are not removed from the DOM + expect( + getComputedStyle(element.shadowRoot.querySelector('.red')).color + ).toEqual('rgb(255, 0, 0)'); + expect( + getComputedStyle(element.shadowRoot.querySelector('.green')).color + ).toEqual('rgb(0, 128, 0)'); + expect(getComputedStyle(element).marginLeft).toEqual('5px'); + } else { + expect( + getComputedStyle(element.shadowRoot.querySelector('.red')).color + ).toEqual('rgb(0, 0, 0)'); + expect( + getComputedStyle(element.shadowRoot.querySelector('.green')).color + ).toEqual('rgb(0, 0, 0)'); + expect(getComputedStyle(element).marginLeft).toEqual('0px'); + } + element.next(); + return new Promise((resolve) => requestAnimationFrame(() => resolve())); + }) + .then(() => { + expect(getComputedStyle(element.shadowRoot.querySelector('.red')).color).toEqual( + 'rgb(255, 0, 0)' + ); + expect(getComputedStyle(element.shadowRoot.querySelector('.green')).color).toEqual( + 'rgb(0, 128, 0)' + ); + expect(getComputedStyle(element).marginLeft).toEqual('5px'); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/a.css b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/a.css new file mode 100644 index 0000000000..91b0d5daea --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/a.css @@ -0,0 +1,3 @@ +.blue { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/a.html b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/a.html new file mode 100644 index 0000000000..60f71823f3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/a.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/b.css b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/b.css new file mode 100644 index 0000000000..75424513d6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/b.css @@ -0,0 +1,3 @@ +.red { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/b.html b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/b.html new file mode 100644 index 0000000000..9663e6a2ed --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/b.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/multi.js b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/multi.js new file mode 100644 index 0000000000..6e524c36b9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multi/multi.js @@ -0,0 +1,16 @@ +import { LightningElement, api } from 'lwc'; +import A from './a.html'; +import B from './b.html'; + +export default class Multi extends LightningElement { + current = A; + + @api + next() { + this.current = this.current === A ? B : A; + } + + render() { + return this.current; + } +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/a.html b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/a.html new file mode 100644 index 0000000000..ad144734c7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/a.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/b.css b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/b.css new file mode 100644 index 0000000000..5d7fdee4f5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/b.css @@ -0,0 +1,11 @@ +.red { + color: red; +} + +:host { + margin-left: 5px; +} + +.green { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/b.html b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/b.html new file mode 100644 index 0000000000..18da8747cb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/b.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/multiNoStyleInFirst.js b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/multiNoStyleInFirst.js new file mode 100644 index 0000000000..4bfcf69f5b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/multiple-templates/x/multiNoStyleInFirst/multiNoStyleInFirst.js @@ -0,0 +1,20 @@ +import { LightningElement, api } from 'lwc'; +import A from './a.html'; +import B from './b.html'; + +export default class MultiNoStyleInFirst extends LightningElement { + current = A; + + @api + next() { + this.current = this.current === A ? B : A; + } + + render() { + return this.current; + } + + renderedCallback() { + this.template.querySelector('.manual').innerHTML = '
        manual
        '; + } +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/index.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/index.spec.js new file mode 100644 index 0000000000..6fb5dc44ca --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/index.spec.js @@ -0,0 +1,18 @@ +import { createElement } from 'lwc'; +import { extractDataIds } from 'test-utils'; +import Grandparent from 'x/grandparent'; + +describe.runIf(process.env.NATIVE_SHADOW)('part and exportparts', () => { + it('supports part and exportparts', () => { + const elm = createElement('x-grandparent', { is: Grandparent }); + document.body.appendChild(elm); + + const ids = extractDataIds(elm); + + return Promise.resolve().then(() => { + expect(getComputedStyle(ids.overlay).color).toEqual('rgb(255, 0, 0)'); + expect(getComputedStyle(ids.text).color).toEqual('rgb(0, 0, 255)'); + expect(getComputedStyle(ids.badge).color).toEqual('rgb(0, 128, 0)'); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/anotherChild/anotherChild.html b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/anotherChild/anotherChild.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/anotherChild/anotherChild.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/anotherChild/anotherChild.js b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/anotherChild/anotherChild.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/anotherChild/anotherChild.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/child/child.html b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/child/child.html new file mode 100644 index 0000000000..5025e8a8b6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/child/child.js b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/child/child.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.css b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.css new file mode 100644 index 0000000000..c2607113a9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.css @@ -0,0 +1,11 @@ +x-parent::part(text) { + color: blue; +} + +x-parent::part(overlay) { + color: red; +} + +x-parent::part(badge) { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.html b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.html new file mode 100644 index 0000000000..26526d92b6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.js b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/grandparent/grandparent.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/parent/parent.html b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/parent/parent.html new file mode 100644 index 0000000000..3153bd6cf7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/parent/parent.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/parent/parent.js b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/parent/parent.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/part-and-exportparts/x/parent/parent.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/index.spec.js b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/index.spec.js new file mode 100644 index 0000000000..ec6ee2909d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/index.spec.js @@ -0,0 +1,54 @@ +import { createElement } from 'lwc'; + +import Parent from 'x/parent'; +import Host from 'x/host'; +import MultiTemplates from 'x/multiTemplates'; + +afterEach(() => { + window.__lwcResetGlobalStylesheets(); +}); + +describe('shadow encapsulation', () => { + it('should not style children elements', () => { + const elm = createElement('x-parent', { is: Parent }); + document.body.appendChild(elm); + + const parentDiv = elm.shadowRoot.querySelector('div'); + expect(window.getComputedStyle(parentDiv).marginLeft).toBe('10px'); + const childDiv = elm.shadowRoot.querySelector('x-child').shadowRoot.querySelector('div'); + expect(window.getComputedStyle(childDiv).marginLeft).toBe('0px'); + }); + + it('should work with multiple templates', () => { + const elm = createElement('x-multi-template', { is: MultiTemplates }); + document.body.appendChild(elm); + + const div = elm.shadowRoot.querySelector('div'); + expect(window.getComputedStyle(div).marginLeft).toBe('10px'); + expect(window.getComputedStyle(div).marginRight).toBe('0px'); + + elm.toggleTemplate(); + return Promise.resolve().then(() => { + const div = elm.shadowRoot.querySelector('div'); + expect(window.getComputedStyle(div).marginLeft).toBe('0px'); + expect(window.getComputedStyle(div).marginRight).toBe('10px'); + }); + }); +}); + +describe(':host', () => { + it('should apply style to the host element', () => { + const elm = createElement('x-host', { is: Host }); + document.body.appendChild(elm); + + expect(window.getComputedStyle(elm).marginLeft).toBe('10px'); + }); + + it('should apply style to the host element with the matching attributes', () => { + const elm = createElement('x-host', { is: Host }); + elm.setAttribute('data-styled', true); + document.body.appendChild(elm); + + expect(window.getComputedStyle(elm).marginLeft).toBe('20px'); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/child/child.html b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/child/child.html new file mode 100644 index 0000000000..36b061865a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/child/child.js b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/child/child.js new file mode 100644 index 0000000000..3f0d7e7e52 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/child/child.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.css b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.css new file mode 100644 index 0000000000..6e17044950 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.css @@ -0,0 +1,8 @@ +:host { + display: block; + margin-left: 10px; +} + +:host([data-styled]) { + margin-left: 20px; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.html b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.html new file mode 100644 index 0000000000..f8c88139b8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.js b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.js new file mode 100644 index 0000000000..204b503a90 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/host/host.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Host extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/a.css b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/a.css new file mode 100644 index 0000000000..32b0d52c99 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/a.css @@ -0,0 +1,4 @@ +div { + margin-left: 10px; + margin-right: 0; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/a.html b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/a.html new file mode 100644 index 0000000000..b8931c2137 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/a.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/b.css b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/b.css new file mode 100644 index 0000000000..d1132c9a86 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/b.css @@ -0,0 +1,4 @@ +div { + margin-left: 0; + margin-right: 10px; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/b.html b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/b.html new file mode 100644 index 0000000000..6c23f51318 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/b.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/multiTemplates.js b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/multiTemplates.js new file mode 100644 index 0000000000..05f533825c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/multiTemplates/multiTemplates.js @@ -0,0 +1,17 @@ +import { LightningElement, api, track } from 'lwc'; + +import tmplA from './a.html'; +import tmplB from './b.html'; + +export default class MultiTemplates extends LightningElement { + @track tmpl = tmplA; + + @api + toggleTemplate() { + this.tmpl = this.tmpl === tmplA ? tmplB : tmplA; + } + + render() { + return this.tmpl; + } +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.css b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.css new file mode 100644 index 0000000000..106d26b08b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.css @@ -0,0 +1,3 @@ +div { + margin-left: 10px; +} diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.html b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.html new file mode 100644 index 0000000000..8311a2adda --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.js b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/stylesheet/x/parent/parent.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js b/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js new file mode 100644 index 0000000000..775661f5a6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js @@ -0,0 +1,248 @@ +import { createElement, setFeatureFlagForTest } from 'lwc'; +import Reactive from 'x/reactive'; +import NonReactive from 'x/nonReactive'; +import Container from 'x/container'; +import Parent from 'x/parent'; +import Child from 'x/child'; +import DuplicateSignalOnTemplate from 'x/duplicateSignalOnTemplate'; +import List from 'x/list'; +import Throws from 'x/throws'; + +// Note for testing purposes the signal implementation uses LWC module resolution to simplify things. +// In production the signal will come from a 3rd party library. +import { Signal } from 'x/signal'; + +describe('signal protocol', () => { + beforeAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); + }); + + afterAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); + }); + + describe('lwc engine subscribes template re-render callback when signal is bound to an LWC and used on a template', () => { + [ + { + testName: 'contains a getter that references a bound signal (.value on template)', + flag: 'showGetterSignal', + }, + { + testName: 'contains a getter that references a bound signal value', + flag: 'showOnlyUsingSignalNotValue', + }, + { + testName: 'contains a signal with @api annotation (.value on template)', + flag: 'showApiSignal', + }, + { + testName: 'contains a signal with @track annotation (.value on template)', + flag: 'showTrackedSignal', + }, + { + testName: 'contains an observed field referencing a signal (.value on template)', + flag: 'showObservedFieldSignal', + }, + { + testName: 'contains a direct reference to a signal (not .value) in the template', + flag: 'showOnlyUsingSignalNotValue', + }, + ].forEach(({ testName, flag }) => { + // Test all ways of binding signal to an LWC + template that cause re-rendering + it(testName, async () => { + const elm = createElement('x-reactive', { is: Reactive }); + document.body.appendChild(elm); + await Promise.resolve(); + + expect(elm.getSignalSubscriberCount()).toBe(0); + elm[flag] = true; + await Promise.resolve(); + + // the engine will automatically subscribe the re-render callback + expect(elm.getSignalSubscriberCount()).toBe(1); + }); + }); + }); + + it('lwc engine should automatically unsubscribe the re-render callback if signal is not used on a template', async () => { + const elm = createElement('x-reactive', { is: Reactive }); + elm.showObservedFieldSignal = true; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(elm.getSignalSubscriberCount()).toBe(1); + elm.showObservedFieldSignal = false; + await Promise.resolve(); + + expect(elm.getSignalSubscriberCount()).toBe(0); + document.body.removeChild(elm); + }); + + it('lwc engine does not subscribe re-render callback if signal is not used on a template', async () => { + const elm = createElement('x-non-reactive', { is: NonReactive }); + document.body.appendChild(elm); + await Promise.resolve(); + + expect(elm.getSignalSubscriberCount()).toBe(0); + }); + + it('only the components referencing a signal should re-render', async () => { + const container = createElement('x-container', { is: Container }); + // append the container first to avoid error message with native lifecycle + document.body.appendChild(container); + await Promise.resolve(); + + const signalElm = createElement('x-signal-elm', { is: Child }); + const signal = new Signal('initial value'); + signalElm.signal = signal; + container.appendChild(signalElm); + await Promise.resolve(); + + expect(container.renderCount).toBe(1); + expect(signalElm.renderCount).toBe(1); + expect(signal.getSubscriberCount()).toBe(1); + + signal.value = 'updated value'; + await Promise.resolve(); + + expect(container.renderCount).toBe(1); + expect(signalElm.renderCount).toBe(2); + expect(signal.getSubscriberCount()).toBe(1); + }); + + it('only subscribes the re-render callback a single time when signal is referenced multiple times on a template', async () => { + const elm = createElement('x-duplicate-signals-on-template', { + is: DuplicateSignalOnTemplate, + }); + document.body.appendChild(elm); + await Promise.resolve(); + + expect(elm.renderCount).toBe(1); + expect(elm.getSignalSubscriberCount()).toBe(1); + expect(elm.getSignalRemovedSubscriberCount()).toBe(0); + + elm.updateSignalValue(); + await Promise.resolve(); + + expect(elm.renderCount).toBe(2); + expect(elm.getSignalSubscriberCount()).toBe(1); + expect(elm.getSignalRemovedSubscriberCount()).toBe(1); + }); + + it('only subscribes re-render callback a single time when signal is referenced multiple times in a list', async () => { + const elm = createElement('x-list', { is: List }); + const signal = new Signal('initial value'); + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(signal.getSubscriberCount()).toBe(1); + expect(signal.getRemovedSubscriberCount()).toBe(0); + + document.body.removeChild(elm); + await Promise.resolve(); + + expect(signal.getSubscriberCount()).toBe(0); + expect(signal.getRemovedSubscriberCount()).toBe(1); + }); + + it('unsubscribes when element is removed from the dom', async () => { + const elm = createElement('x-child', { is: Child }); + const signal = new Signal('initial value'); + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(signal.getSubscriberCount()).toBe(1); + expect(signal.getRemovedSubscriberCount()).toBe(0); + + document.body.removeChild(elm); + await Promise.resolve(); + + expect(signal.getSubscriberCount()).toBe(0); + expect(signal.getRemovedSubscriberCount()).toBe(1); + }); + + it('on template re-render unsubscribes all components where signal is not present on the template', async () => { + const elm = createElement('x-parent', { is: Parent }); + elm.showChild = true; + + document.body.appendChild(elm); + await Promise.resolve(); + + // subscribed both parent and child + // as long as parent contains reference to the signal, even if it's just to pass it to a child + // it will be subscribed. + expect(elm.getSignalSubscriberCount()).toBe(2); + expect(elm.getSignalRemovedSubscriberCount()).toBe(0); + + elm.showChild = false; + await Promise.resolve(); + + // The signal is not being used on the parent template anymore so it will be removed + expect(elm.getSignalSubscriberCount()).toBe(0); + expect(elm.getSignalRemovedSubscriberCount()).toBe(2); + }); + + it('does not subscribe if the signal shape is incorrect', async () => { + const elm = createElement('x-child', { is: Child }); + const subscribe = jasmine.createSpy(); + // Note the signals property is value's' and not value + const signal = { values: 'initial value', subscribe }; + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(subscribe).not.toHaveBeenCalled(); + }); + + it('does not subscribe if the signal is not added as trusted signal', async () => { + const elm = createElement('x-child', { is: Child }); + const subscribe = jasmine.createSpy(); + // Note this follows the shape of the signal implementation + // but it's not added as a trusted signal (add using lwc.addTrustedSignal) + const signal = { + get value() { + return 'initial value'; + }, + subscribe, + }; + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(subscribe).not.toHaveBeenCalled(); + }); + + it('does not throw an error for objects that throw upon "in" checks', async () => { + const elm = createElement('x-throws', { is: Throws }); + document.body.appendChild(elm); + + await Promise.resolve(); + + expect(elm.shadowRoot.querySelector('h1').textContent).toBe('hello'); + }); +}); + +describe('ENABLE_EXPERIMENTAL_SIGNALS not set', () => { + beforeAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); + }); + + it('does not subscribe or unsubscribe if feature flag is disabled', async () => { + const elm = createElement('x-child', { is: Child }); + const signal = new Signal('initial value'); + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(signal.getSubscriberCount()).toBe(0); + expect(signal.getRemovedSubscriberCount()).toBe(0); + + document.body.removeChild(elm); + await Promise.resolve(); + + expect(signal.getSubscriberCount()).toBe(0); + expect(signal.getRemovedSubscriberCount()).toBe(0); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/child/child.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/child/child.html new file mode 100644 index 0000000000..2428dd9e7c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/child/child.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/child/child.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/child/child.js new file mode 100644 index 0000000000..76a657a00f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/child/child.js @@ -0,0 +1,10 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api renderCount = 0; + @api signal; + + renderedCallback() { + this.renderCount++; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/container/container.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/container/container.html new file mode 100644 index 0000000000..fba6288c0b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/container/container.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/container/container.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/container/container.js new file mode 100644 index 0000000000..dbb973cc07 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/container/container.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api renderCount = 0; + + renderedCallback() { + this.renderCount++; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/duplicateSignalOnTemplate/duplicateSignalOnTemplate.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/duplicateSignalOnTemplate/duplicateSignalOnTemplate.html new file mode 100644 index 0000000000..9e7f95c88d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/duplicateSignalOnTemplate/duplicateSignalOnTemplate.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/duplicateSignalOnTemplate/duplicateSignalOnTemplate.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/duplicateSignalOnTemplate/duplicateSignalOnTemplate.js new file mode 100644 index 0000000000..67f2d836e1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/duplicateSignalOnTemplate/duplicateSignalOnTemplate.js @@ -0,0 +1,26 @@ +import { LightningElement, api } from 'lwc'; +import { Signal } from 'x/signal'; + +export default class extends LightningElement { + signal = new Signal('initial value'); + @api renderCount = 0; + + renderedCallback() { + this.renderCount++; + } + + @api + getSignalSubscriberCount() { + return this.signal.getSubscriberCount(); + } + + @api + getSignalRemovedSubscriberCount() { + return this.signal.getRemovedSubscriberCount(); + } + + @api + updateSignalValue() { + this.signal.value = 'updated value'; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/list/list.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/list/list.html new file mode 100644 index 0000000000..8dc4e54056 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/list/list.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/list/list.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/list/list.js new file mode 100644 index 0000000000..2b6333734e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/list/list.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api signal; + items = [1, 2, 3, 4, 5, 6]; +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/nonReactive/nonReactive.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/nonReactive/nonReactive.html new file mode 100644 index 0000000000..6505517bb7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/nonReactive/nonReactive.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/nonReactive/nonReactive.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/nonReactive/nonReactive.js new file mode 100644 index 0000000000..5face7363b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/nonReactive/nonReactive.js @@ -0,0 +1,22 @@ +import { LightningElement, api, track } from 'lwc'; +import { Signal } from 'x/signal'; + +const signal = new Signal('initial value'); + +export default class extends LightningElement { + // Note that this signal is bound but it's never referenced on the template + _signal = signal; + @api apiSignalValue = signal.value; + @track trackSignalValue = signal.value; + observedFieldExternalSignalValue = signal.value; + observedFieldBoundSignalValue = this._signal.value; + + get externalSignalValueGetter() { + return signal.value; + } + + @api + getSignalSubscriberCount() { + return signal.getSubscriberCount(); + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/parent/parent.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/parent/parent.html new file mode 100644 index 0000000000..279373129a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/parent/parent.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/parent/parent.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/parent/parent.js new file mode 100644 index 0000000000..c7c7c288c3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/parent/parent.js @@ -0,0 +1,28 @@ +import { LightningElement, api } from 'lwc'; +import { Signal } from 'x/signal'; + +export default class extends LightningElement { + signal = new Signal('initial value'); + + @api showChild = false; + @api renderCount = 0; + + renderedCallback() { + this.renderCount++; + } + + @api + getSignalSubscriberCount() { + return this.signal.getSubscriberCount(); + } + + @api + getSignalRemovedSubscriberCount() { + return this.signal.getRemovedSubscriberCount(); + } + + @api + updateSignalValue() { + this.signal.value = 'updated value'; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/reactive/reactive.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/reactive/reactive.html new file mode 100644 index 0000000000..d0dc32ccf4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/reactive/reactive.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/reactive/reactive.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/reactive/reactive.js new file mode 100644 index 0000000000..625b406b0f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/reactive/reactive.js @@ -0,0 +1,32 @@ +import { LightningElement, api, track } from 'lwc'; +import { Signal } from 'x/signal'; + +const signal = new Signal('initial value'); + +export default class extends LightningElement { + @api showApiSignal = false; + @api showGetterSignal = false; + @api showGetterSignalValue = false; + @api showTrackedSignal = false; + @api showObservedFieldSignal = false; + @api showOnlyUsingSignalNotValue = false; + + @api apiSignal = signal; + @track trackSignal = signal; + + observedFieldSignal = signal; + + get getterSignalField() { + // this works because the signal is bound to the LWC + return this.observedFieldSignal; + } + + get getterSignalFieldValue() { + return this.observedFieldSignal.value; + } + + @api + getSignalSubscriberCount() { + return signal.getSubscriberCount(); + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/signal/signal.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/signal/signal.js new file mode 100644 index 0000000000..a88e8a5ef3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/signal/signal.js @@ -0,0 +1,45 @@ +// Note for testing purposes the signal implementation uses LWC module resolution to simplify things. +// In production the signal will come from a 3rd party library. + +import { addTrustedSignal } from 'test-utils'; + +export class Signal { + subscribers = new Set(); + removedSubscribers = []; + + constructor(initialValue) { + this._value = initialValue; + addTrustedSignal(this); + } + + set value(newValue) { + this._value = newValue; + this.notify(); + } + + get value() { + return this._value; + } + + subscribe(onUpdate) { + this.subscribers.add(onUpdate); + return () => { + this.subscribers.delete(onUpdate); + this.removedSubscribers.push(onUpdate); + }; + } + + notify() { + for (const subscriber of this.subscribers) { + subscriber(); + } + } + + getSubscriberCount() { + return this.subscribers.size; + } + + getRemovedSubscriberCount() { + return this.removedSubscribers.length; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/throws/throws.html b/packages/@lwc/integration-not-karma/test/signal/protocol/x/throws/throws.html new file mode 100644 index 0000000000..e40c3bd251 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/throws/throws.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/x/throws/throws.js b/packages/@lwc/integration-not-karma/test/signal/protocol/x/throws/throws.js new file mode 100644 index 0000000000..21f0811441 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/x/throws/throws.js @@ -0,0 +1,23 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + foo; + + constructor() { + super(); + + this.foo = new Proxy( + {}, + { + has() { + throw new Error("oh no you don't!"); + }, + } + ); + } + + renderedCallback() { + // access `this.foo` to trigger mutation-tracker.ts + this.bar = this.foo; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/index.spec.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/index.spec.js new file mode 100644 index 0000000000..6583037e4b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/index.spec.js @@ -0,0 +1,105 @@ +import { createElement, setFeatureFlagForTest } from 'lwc'; + +import Reactive from 'x/reactive'; +import NonReactive from 'x/nonReactive'; +import ExplicitSubscribe from 'x/explicitSubscribe'; +import List from 'x/list'; + +// Note for testing purposes the signal implementation uses LWC module resolution to simplify things. +// In production the signal will come from a 3rd party library. +import { Signal } from 'x/signal'; + +const createElementSignalAndInsertIntoDom = async (tagName, ctor, signalInitialValue) => { + const elm = createElement(tagName, { is: ctor }); + const signal = new Signal(signalInitialValue); + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + return { elm, signal }; +}; + +describe('signal reaction in lwc', () => { + beforeAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); + }); + + afterAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); + }); + + it('should render signal value', async () => { + const { elm } = await createElementSignalAndInsertIntoDom( + 'x-reactive', + Reactive, + 'initial value' + ); + + expect(elm.shadowRoot.textContent).toBe('initial value'); + }); + + it('should re-render when signal notification is sent', async () => { + const { elm, signal } = await createElementSignalAndInsertIntoDom( + 'x-reactive', + Reactive, + 'initial value' + ); + + expect(elm.shadowRoot.textContent).toBe('initial value'); + + // notification happens when value is updated + signal.value = 'updated value'; + await Promise.resolve(); + + expect(elm.shadowRoot.textContent).toEqual('updated value'); + }); + + it('does not re-render when signal is not bound to an LWC', async () => { + const elm = createElement('x-non-reactive', { is: NonReactive }); + document.body.appendChild(elm); + await Promise.resolve(); + + expect(elm.shadowRoot.textContent).toBe('external signal value'); + + elm.updateExternalSignal(); + await Promise.resolve(); + + expect(elm.shadowRoot.textContent).toBe('external signal value'); + }); + + it('should be able to re-render when manually subscribing to signal', async () => { + const { elm, signal } = await createElementSignalAndInsertIntoDom( + 'x-manual-subscribe', + ExplicitSubscribe, + 'initial value' + ); + expect(elm.shadowRoot.textContent).toEqual('default'); + + signal.value = 'new value'; + await Promise.resolve(); + + expect(elm.shadowRoot.textContent).toEqual('new value'); + }); + + it('render lists properly', async () => { + const { elm, signal } = await createElementSignalAndInsertIntoDom( + 'x-reactive-list', + List, + [1, 2, 3] + ); + + expect(elm.shadowRoot.children.length).toBe(3); + expect(elm.shadowRoot.children[0].textContent).toBe('1'); + expect(elm.shadowRoot.children[1].textContent).toBe('2'); + expect(elm.shadowRoot.children[2].textContent).toBe('3'); + + signal.value = [3, 2, 1]; + + await Promise.resolve(); + + expect(elm.shadowRoot.children.length).toBe(3); + expect(elm.shadowRoot.children[0].textContent).toBe('3'); + expect(elm.shadowRoot.children[1].textContent).toBe('2'); + expect(elm.shadowRoot.children[2].textContent).toBe('1'); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/explicitSubscribe/explicitSubscribe.html b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/explicitSubscribe/explicitSubscribe.html new file mode 100644 index 0000000000..6df6f20a58 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/explicitSubscribe/explicitSubscribe.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/explicitSubscribe/explicitSubscribe.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/explicitSubscribe/explicitSubscribe.js new file mode 100644 index 0000000000..24106084e4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/explicitSubscribe/explicitSubscribe.js @@ -0,0 +1,21 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api signal; + + foo = 'default'; + + signalUnsubscribe = () => {}; + + connectedCallback() { + this.signalUnsubscribe = this.signal.subscribe(() => this.updateOnSignalNotification()); + } + + disconnectedCallback() { + this.signalUnsubscribe(); + } + + updateOnSignalNotification() { + this.foo = this.signal.value; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/list/list.html b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/list/list.html new file mode 100644 index 0000000000..51c0f0998b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/list/list.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/list/list.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/list/list.js new file mode 100644 index 0000000000..b4ecf8087f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/list/list.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api signal; +} diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/nonReactive/nonReactive.html b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/nonReactive/nonReactive.html new file mode 100644 index 0000000000..09ba2ab0bf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/nonReactive/nonReactive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/nonReactive/nonReactive.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/nonReactive/nonReactive.js new file mode 100644 index 0000000000..850199c81a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/nonReactive/nonReactive.js @@ -0,0 +1,15 @@ +import { LightningElement, api } from 'lwc'; +import { Signal } from 'x/signal'; + +const externalSignal = new Signal('external signal value'); + +export default class extends LightningElement { + get bar() { + return externalSignal.value; + } + + @api + updateExternalSignal() { + externalSignal.value = 'updated external value'; + } +} diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactive/reactive.html b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactive/reactive.html new file mode 100644 index 0000000000..7f27bfba6f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactive/reactive.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactive/reactive.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactive/reactive.js new file mode 100644 index 0000000000..41eafcc1a0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactive/reactive.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + signal; +} diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactiveSubscriber/reactiveSubscriber.html b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactiveSubscriber/reactiveSubscriber.html new file mode 100644 index 0000000000..2da7be3d7f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactiveSubscriber/reactiveSubscriber.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactiveSubscriber/reactiveSubscriber.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactiveSubscriber/reactiveSubscriber.js new file mode 100644 index 0000000000..41eafcc1a0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/reactiveSubscriber/reactiveSubscriber.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + signal; +} diff --git a/packages/@lwc/integration-not-karma/test/signal/reactivity/x/signal/signal.js b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/signal/signal.js new file mode 100644 index 0000000000..21fd9ea483 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/reactivity/x/signal/signal.js @@ -0,0 +1,39 @@ +// Note for testing purposes the signal implementation uses LWC module resolution to simplify things. +// In production the signal will come from a 3rd party library. + +import { addTrustedSignal } from 'test-utils'; + +export class Signal { + subscribers = new Set(); + + constructor(initialValue) { + this._value = initialValue; + addTrustedSignal(this); + } + + set value(newValue) { + this._value = newValue; + this.notify(); + } + + get value() { + return this._value; + } + + subscribe(onUpdate) { + this.subscribers.add(onUpdate); + return () => { + this.subscribers.delete(onUpdate); + }; + } + + notify() { + for (const subscriber of this.subscribers) { + subscriber(); + } + } + + getSubscriberCount() { + return this.subscribers.size; + } +} diff --git a/packages/@lwc/integration-not-karma/test/spread/index.spec.js b/packages/@lwc/integration-not-karma/test/spread/index.spec.js new file mode 100644 index 0000000000..68f69035d1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/spread/index.spec.js @@ -0,0 +1,117 @@ +import { createElement } from 'lwc'; +import Test from 'x/test'; +import { getHooks, setHooks } from 'test-utils'; + +function setSanitizeHtmlContentHookForTest(impl) { + const { sanitizeHtmlContent } = getHooks(); + + setHooks({ + sanitizeHtmlContent: impl, + }); + + return sanitizeHtmlContent; +} +describe('lwc:spread', () => { + let elm, simpleChild, overriddenChild, trackedChild, innerHTMLChild, originalHook, consoleSpy; + beforeEach(() => { + consoleSpy = spyOn(console, 'warn'); + originalHook = setSanitizeHtmlContentHookForTest((x) => x); + elm = createElement('x-test', { is: Test }); + document.body.appendChild(elm); + simpleChild = elm.shadowRoot.querySelector('.x-child-simple'); + overriddenChild = elm.shadowRoot.querySelector('.x-child-overridden'); + trackedChild = elm.shadowRoot.querySelector('.x-child-tracked'); + innerHTMLChild = elm.shadowRoot.querySelector('.div-innerhtml'); + spyOn(console, 'log'); + }); + afterEach(() => { + setSanitizeHtmlContentHookForTest(originalHook); + }); + it('should render basic test', () => { + expect(simpleChild.shadowRoot.querySelector('span').textContent).toEqual('Name: LWC'); + }); + it('should not override innerHTML from inner-html directive', () => { + expect(innerHTMLChild.innerHTML).toEqual(''); + + if (process.env.NODE_ENV === 'production') { + expect(consoleSpy).not.toHaveBeenCalled(); + } else { + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy.calls.argsFor(0)[0].message).toContain( + `Cannot set property "innerHTML". Instead, use lwc:inner-html or lwc:dom-manual.` + ); + } + }); + it('should assign onclick', () => { + simpleChild.click(); + // eslint-disable-next-line no-console + expect(console.log).toHaveBeenCalledWith('spread click called', simpleChild); + }); + it('should override values in template', async () => { + expect(overriddenChild.shadowRoot.querySelector('span').textContent).toEqual('Name: Aura'); + elm.modify(function () { + this.overriddenProps = {}; + }); + await Promise.resolve(); + expect(overriddenChild.shadowRoot.querySelector('span').textContent).toEqual('Name: lwc'); + }); + it('should assign onclick along with the one in template', () => { + overriddenChild.click(); + // eslint-disable-next-line no-console + expect(console.log).toHaveBeenCalledWith('spread click called', overriddenChild); + // eslint-disable-next-line no-console + expect(console.log).toHaveBeenCalledWith( + 'template click called', + jasmine.any(Object) /* component */ + ); + }); + + it('should assign props to standard elements', async () => { + expect(elm.shadowRoot.querySelector('span').className).toEqual('spanclass'); + + elm.modify(function () { + this.spanProps = { className: 'spanclass2' }; + }); + await Promise.resolve(); + expect(elm.shadowRoot.querySelector('span').className).toEqual('spanclass2'); + + elm.modify(function () { + this.spanProps = {}; + }); + await Promise.resolve(); + expect(elm.shadowRoot.querySelector('span').className).toEqual('spanclass2'); + + elm.modify(function () { + this.spanProps = { className: undefined }; + }); + await Promise.resolve(); + expect(elm.shadowRoot.querySelector('span').className).toEqual('undefined'); + + elm.modify(function () { + this.spanProps = { className: '' }; + }); + await Promise.resolve(); + expect(elm.shadowRoot.querySelector('span').className).toEqual(''); + }); + it('should assign props to dynamic elements using lwc:dynamic', () => { + expect( + elm.shadowRoot.querySelector('x-cmp').shadowRoot.querySelector('span').textContent + ).toEqual('Name: Dynamic'); + }); + it('should assign props to dynamic elements', () => { + expect( + elm.shadowRoot + .querySelector('[data-id="lwc-component"]') + .shadowRoot.querySelector('span').textContent + ).toEqual('Name: Dynamic'); + }); + + it('should rerender when tracked props are assigned', async () => { + expect(trackedChild.shadowRoot.querySelector('span').textContent).toEqual('Name: Tracked'); + elm.modify(function () { + this.trackedProps.name = 'Altered'; + }); + await Promise.resolve(); + expect(trackedChild.shadowRoot.querySelector('span').textContent).toEqual('Name: Altered'); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/spread/x/child/child.html b/packages/@lwc/integration-not-karma/test/spread/x/child/child.html new file mode 100644 index 0000000000..2e0fb89086 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/spread/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/spread/x/child/child.js b/packages/@lwc/integration-not-karma/test/spread/x/child/child.js new file mode 100644 index 0000000000..2803088a60 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/spread/x/child/child.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Child extends LightningElement { + @api name; +} diff --git a/packages/@lwc/integration-not-karma/test/spread/x/test/test.html b/packages/@lwc/integration-not-karma/test/spread/x/test/test.html new file mode 100644 index 0000000000..9a2aa1505f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/spread/x/test/test.html @@ -0,0 +1,9 @@ + diff --git a/packages/@lwc/integration-not-karma/test/spread/x/test/test.js b/packages/@lwc/integration-not-karma/test/spread/x/test/test.js new file mode 100644 index 0000000000..d5e2c852d7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/spread/x/test/test.js @@ -0,0 +1,27 @@ +import { api, LightningElement, track } from 'lwc'; +import Child from 'x/child'; + +export default class Test extends LightningElement { + simpleProps = { name: 'LWC', onclick: this.spreadClick }; + overriddenProps = { name: 'Aura', onclick: this.spreadClick }; + spanProps = { className: 'spanclass' }; + dynamicCtor = Child; + dynamicProps = { name: 'Dynamic' }; + @track trackedProps = { name: 'Tracked' }; + innerHTMLProps = { innerHTML: 'innerHTML from spread' }; + innerHTML = 'innerHTML from directive'; + + spreadClick() { + // eslint-disable-next-line no-console + console.log('spread click called', this); + } + + templateClick() { + // eslint-disable-next-line no-console + console.log('template click called', this); + } + + @api modify(fn) { + fn.call(this); + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/index.spec.js b/packages/@lwc/integration-not-karma/test/static-content/index.spec.js new file mode 100644 index 0000000000..07945e00fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/index.spec.js @@ -0,0 +1,865 @@ +import { createElement } from 'lwc'; +import { extractDataIds, LOWERCASE_SCOPE_TOKENS } from 'test-utils'; +import Container from 'x/container'; +import Escape from 'x/escape'; +import MultipleStyles from 'x/multipleStyles'; +import SvgNs from 'x/svgNs'; +import Table from 'x/table'; +import SvgPath from 'x/svgPath'; +import SvgPathInDiv from 'x/svgPathInDiv'; +import SvgPathInG from 'x/svgPathInG'; +import StaticUnsafeTopLevel from 'x/staticUnsafeTopLevel'; +import OnlyEventListener from 'x/onlyEventListener'; +import OnlyEventListenerChild from 'x/onlyEventListenerChild'; +import OnlyEventListenerGrandchild from 'x/onlyEventListenerGrandchild'; +import ListenerStaticWithUpdates from 'x/listenerStaticWithUpdates'; +import DeepListener from 'x/deepListener'; +import Comments from 'x/comments'; +import PreserveComments from 'x/preserveComments'; +import Attribute from 'x/attribute'; +import DeepAttribute from 'x/deepAttribute'; +import IframeOnload from 'x/iframeOnload'; +import WithKey from 'x/withKey'; +import Text from 'x/text'; +import TableWithExpression from 'x/tableWithExpressions'; +import TextWithoutPreserveComments from 'x/textWithoutPreserveComments'; +import TextWithPreserveComments from 'x/textWithPreserveComments'; + +describe.skipIf(process.env.NATIVE_SHADOW)('Mixed mode for static content', () => { + ['native', 'synthetic'].forEach((firstRenderMode) => { + it(`should set the tokens for synthetic shadow when it renders first in ${firstRenderMode}`, () => { + const elm = createElement('x-container', { is: Container }); + elm.syntheticFirst = firstRenderMode === 'synthetic'; + document.body.appendChild(elm); + + const syntheticMode = elm.shadowRoot + .querySelector('x-component') + .shadowRoot.querySelector('div'); + const nativeMode = elm.shadowRoot + .querySelector('x-native') + .shadowRoot.querySelector('x-component') + .shadowRoot.querySelector('div'); + + const token = LOWERCASE_SCOPE_TOKENS ? 'lwc-6a8uqob2ku4' : 'x-component_component'; + expect(syntheticMode.hasAttribute(token)).toBe(true); + expect(nativeMode.hasAttribute(token)).toBe(false); + }); + }); +}); + +describe('static content when stylesheets change', () => { + it('should reflect correct token for scoped styles', () => { + const elm = createElement('x-container', { is: MultipleStyles }); + + const stylesheetsWarning = + /Mutating the "stylesheets" property on a template is deprecated and will be removed in a future version of LWC/; + + expect(() => { + elm.updateTemplate({ + name: 'a', + useScopedCss: false, + }); + }).toLogWarningDev(stylesheetsWarning); + + window.__lwcResetAlreadyLoggedMessages(); + + document.body.appendChild(elm); + + expect(elm.shadowRoot.querySelector('div').getAttribute('class')).toBe('foo'); + + // atm, we need to switch templates. + expect(() => { + elm.updateTemplate({ + name: 'b', + useScopedCss: true, + }); + }).toLogWarningDev(stylesheetsWarning); + + window.__lwcResetAlreadyLoggedMessages(); + + return Promise.resolve() + .then(() => { + const classList = Array.from(elm.shadowRoot.querySelector('div').classList).sort(); + expect(classList).toEqual([ + 'foo', + LOWERCASE_SCOPE_TOKENS ? 'lwc-6fpm08fjoch' : 'x-multipleStyles_b', + ]); + + expect(() => { + elm.updateTemplate({ + name: 'a', + useScopedCss: false, + }); + }).toLogWarningDev(stylesheetsWarning); + }) + .then(() => { + const classList = Array.from(elm.shadowRoot.querySelector('div').classList).sort(); + expect(classList).toEqual(['foo']); + }); + }); +}); + +describe('svg and static content', () => { + it('should use correct namespace', () => { + const elm = createElement('x-svg-ns', { is: SvgNs }); + document.body.appendChild(elm); + + const allStaticNodes = elm.querySelectorAll('.static'); + + allStaticNodes.forEach((node) => { + expect(node.namespaceURI).toBe('http://www.w3.org/2000/svg'); + }); + }); + + function getDomStructure(elm) { + const tagName = elm.tagName.toLowerCase(); + const result = { tagName }; + for (let i = 0; i < elm.children.length; i++) { + const child = elm.children[i]; + result.children = result.children || []; + result.children.push(getDomStructure(child)); + } + return result; + } + + it('should correctly parse ', () => { + const elm = createElement('x-svg-path', { is: SvgPath }); + document.body.appendChild(elm); + + expect(getDomStructure(elm.shadowRoot.firstChild)).toEqual({ + tagName: 'svg', + children: [ + { + tagName: 'path', + }, + { + tagName: 'path', + }, + ], + }); + }); + + it('should correctly parse in div', () => { + const elm = createElement('x-svg-path-in-div', { is: SvgPathInDiv }); + document.body.appendChild(elm); + + expect(getDomStructure(elm.shadowRoot.firstChild)).toEqual({ + tagName: 'div', + children: [ + { + tagName: 'svg', + children: [ + { + tagName: 'path', + }, + { + tagName: 'path', + }, + ], + }, + ], + }); + }); + + it('should correctly parse in ', () => { + const elm = createElement('x-svg-path-in-g', { is: SvgPathInG }); + document.body.appendChild(elm); + + expect(getDomStructure(elm.shadowRoot.firstChild)).toEqual({ + tagName: 'svg', + children: [ + { + tagName: 'g', + children: [ + { + tagName: 'path', + }, + { + tagName: 'path', + }, + ], + }, + ], + }); + }); +}); + +describe('elements that cannot be parsed as top-level', () => { + it('should work with a static ', () => { + const elm = createElement('x-table', { is: Table }); + document.body.appendChild(elm); + + expect(elm.shadowRoot.querySelectorAll('td').length).toEqual(0); + + elm.addRow(); + + return Promise.resolve().then(() => { + expect(elm.shadowRoot.querySelectorAll('td').length).toEqual(1); + expect(elm.shadowRoot.querySelector('td').textContent).toEqual(''); + }); + }); + + it('works for all elements that cannot be safely parsed as top-level', () => { + const elm = createElement('x-static-unsafe-top-level', { is: StaticUnsafeTopLevel }); + document.body.appendChild(elm); + + const getChildrenTagNames = () => { + const result = []; + const { children } = elm.shadowRoot; + for (let i = 0; i < children.length; i++) { + result.push(children[i].tagName.toLowerCase()); + } + return result; + }; + + const expectedChildren = [ + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr', + ]; + + expect(getChildrenTagNames()).toEqual([]); + elm.doRender = true; + return Promise.resolve() + .then(() => { + expect(getChildrenTagNames()).toEqual(expectedChildren); + elm.doRender = false; + }) + .then(() => { + expect(getChildrenTagNames()).toEqual([]); + elm.doRender = true; + }) + .then(() => { + expect(getChildrenTagNames()).toEqual(expectedChildren); + }); + }); +}); + +describe('template literal escaping', () => { + it('should properly render escaped content', () => { + const elm = createElement('x-escape', { is: Escape }); + document.body.appendChild(elm); + + // "`" + [ + () => elm.shadowRoot.querySelector('.backtick-text').textContent, + () => elm.shadowRoot.querySelector('.backtick-comment').firstChild.textContent, + () => elm.shadowRoot.querySelector('.backtick-attr').getAttribute('data-message'), + ].forEach((selector) => { + expect(selector()).toBe('Escape `me`'); + }); + + // "\`" + [ + () => elm.shadowRoot.querySelector('.backtick-escape-text').textContent, + () => elm.shadowRoot.querySelector('.backtick-escape-comment').firstChild.textContent, + () => + elm.shadowRoot.querySelector('.backtick-escape-attr').getAttribute('data-message'), + ].forEach((selector) => { + expect(selector()).toBe('Escape \\`me`'); + }); + + // "${" + expect(elm.shadowRoot.querySelector('.dollar-attr').getAttribute('data-message')).toBe( + 'Escape ${me}' + ); + + // "\${" + expect( + elm.shadowRoot.querySelector('.dollar-escape-attr').getAttribute('data-message') + ).toBe('Escape \\${me}'); + }); +}); + +describe('static optimization with event listeners', () => { + // We test an event listener on the self, child, and grandchild, because we currently + // cannot optimize event listeners anywhere except at the top level of a static fragment. + // So we need to ensure that potentially-static parents/grandparents do not result in + // event listeners not being attached incorrectly. + const scenarios = [ + { + name: 'self', + Component: OnlyEventListener, + }, + { + name: 'child', + Component: OnlyEventListenerChild, + }, + { + name: 'grandchild', + Component: OnlyEventListenerGrandchild, + }, + ]; + + scenarios.forEach(({ name, Component }) => { + describe(name, () => { + // CustomEvent is not supported in IE11 + const CE = typeof CustomEvent === 'function' ? CustomEvent : Event; + + let elm; + let button; + + beforeEach(async () => { + elm = createElement('x-only-event-listener', { is: Component }); + document.body.appendChild(elm); + + await Promise.resolve(); + + button = elm.shadowRoot.querySelector('button'); + }); + + it('works with element that is static except for event listener', async () => { + button.dispatchEvent(new CE('foo')); + button.dispatchEvent(new CE('bar')); + expect(elm.counts).toEqual({ foo: 1, bar: 1 }); + + // trigger re-render + elm.dynamic = 'yolo'; + + await Promise.resolve(); + + button.dispatchEvent(new CE('foo')); + button.dispatchEvent(new CE('bar')); + expect(elm.counts).toEqual({ foo: 2, bar: 2 }); + }); + + it('can have manual listeners too', async () => { + const dispatcher = jasmine.createSpy(); + + button.addEventListener('baz', dispatcher); + button.dispatchEvent(new CE('baz')); + expect(dispatcher.calls.count()).toBe(1); + + // trigger re-render + elm.dynamic = 'yolo'; + + await Promise.resolve(); + + button.dispatchEvent(new CE('baz')); + expect(dispatcher.calls.count()).toBe(2); + }); + }); + }); +}); + +describe('event listeners on static nodes when other nodes are updated', () => { + it('event listeners work after updates', async () => { + const elm = createElement('x-listener-static-with-updates', { + is: ListenerStaticWithUpdates, + }); + document.body.appendChild(elm); + + await Promise.resolve(); + + let expectedCount = 0; + + expect(elm.fooEventCount).toBe(expectedCount); + elm.fireFooEvent(); + expect(elm.fooEventCount).toBe(++expectedCount); + + await Promise.resolve(); + for (let i = 0; i < 3; i++) { + elm.version = i; + elm.fireFooEvent(); + expect(elm.fooEventCount).toBe(++expectedCount); + await Promise.resolve(); + elm.fireFooEvent(); + expect(elm.fooEventCount).toBe(++expectedCount); + } + }); +}); + +describe('event listeners on deep paths', () => { + it('handles events correctly', async () => { + const elm = createElement('x-deep-listener', { + is: DeepListener, + }); + document.body.appendChild(elm); + + await Promise.resolve(); + + let count = 0; + expect(elm.counter).toBe(count); + + const childElms = Object.values(extractDataIds(elm)); + expect(childElms.length).toBe(12); // static1, dynamic1, deepStatic1, static2, etc. until 4 + + for (const childElm of childElms) { + childElm.dispatchEvent(new CustomEvent('foo')); + expect(elm.counter).toBe(++count); + } + }); +}); + +describe('static parts applies to comments correctly', () => { + it('has correct static parts when lwc:preserve-comments is off', async () => { + const elm = createElement('x-comments', { + is: Comments, + }); + document.body.appendChild(elm); + + await Promise.resolve(); + + const { foo, bar } = extractDataIds(elm); + const refs = elm.getRefs(); + + foo.click(); + expect(elm.fooWasClicked).toBe(true); + expect(refs.foo).toBe(foo); + + bar.click(); + expect(elm.barWasClicked).toBe(true); + expect(refs.bar).toBe(bar); + }); + + it('has correct static parts when lwc:preserve-comments is on', async () => { + const elm = createElement('x-preserve-comments', { + is: PreserveComments, + }); + document.body.appendChild(elm); + + await Promise.resolve(); + + const { foo, bar } = extractDataIds(elm); + const refs = elm.getRefs(); + + foo.click(); + expect(elm.fooWasClicked).toBe(true); + expect(refs.foo).toBe(foo); + + bar.click(); + expect(elm.barWasClicked).toBe(true); + expect(refs.bar).toBe(bar); + }); +}); + +describe('static content optimization with attribute', () => { + let nodes = {}; + let elm; + + beforeEach(async () => { + elm = createElement('x-attributes', { is: Attribute }); + document.body.appendChild(elm); + await Promise.resolve(); + nodes = extractDataIds(elm); + }); + + const verifyStyleAttributeAppliedCorrectly = ({ cmp, expected }) => + expect(cmp.getAttribute('style')).toEqual(expected); + + const verifyAttributeAppliedCorrectly = ({ cmp, expected }) => + expect(cmp.getAttribute('data-value')).toEqual(expected); + + const verifyClassAppliedCorrectly = ({ cmp, expected }) => + expect(cmp.getAttribute('class')).toEqual(expected); + + it('preserves static values', () => { + const { + staticAttr, + staticAttrNested, + staticClass, + staticClassBoolean, + staticClassEmpty, + staticClassNested, + staticClassSpaces, + staticClassTab, + staticClassTabs, + staticCombined, + staticCombinedNested, + staticStyle, + staticStyleBoolean, + staticStyleEmpty, + staticStyleInvalid, + staticStyleNested, + staticStyleSpaces, + staticStyleTab, + staticStyleTabs, + } = nodes; + + // styles + [ + { cmp: staticStyle, expected: 'color: blue;' }, + { cmp: staticCombined, expected: 'color: red;' }, + { cmp: staticStyleNested, expected: 'color: white;' }, + { cmp: staticCombinedNested, expected: 'color: orange;' }, + { cmp: staticStyleBoolean, expected: null }, + { cmp: staticStyleEmpty, expected: null }, + { cmp: staticStyleInvalid, expected: null }, + { cmp: staticStyleSpaces, expected: null }, + { cmp: staticStyleTab, expected: null }, + { cmp: staticStyleTabs, expected: null }, + ].forEach(verifyStyleAttributeAppliedCorrectly); + + // class + [ + { cmp: staticClass, expected: 'static class' }, + { cmp: staticCombined, expected: 'combined class' }, + { cmp: staticClassNested, expected: 'static nested class' }, + { cmp: staticCombinedNested, expected: 'static combined nested' }, + { cmp: staticClassBoolean, expected: null }, + { cmp: staticClassEmpty, expected: null }, + { cmp: staticClassSpaces, expected: null }, + { cmp: staticClassTab, expected: null }, + { cmp: staticClassTabs, expected: null }, + ].forEach(verifyClassAppliedCorrectly); + + // attributes + [ + { cmp: staticAttr, expected: 'static1' }, + { cmp: staticCombined, expected: 'static2' }, + { cmp: staticAttrNested, expected: 'static3' }, + { cmp: staticCombinedNested, expected: 'static4' }, + ].forEach(verifyAttributeAppliedCorrectly); + }); + + it('applies expressions on mount', () => { + const { + dynamicAttr, + dynamicStyle, + dynamicClass, + dynamicAttrNested, + dynamicStyleNested, + dynamicClassNested, + dynamicCombined, + dynamicCombinedNested, + } = nodes; + + // styles + [ + { cmp: dynamicStyle, expected: 'color: green;' }, + { cmp: dynamicStyleNested, expected: 'color: violet;' }, + { cmp: dynamicCombined, expected: 'color: orange;' }, + { cmp: dynamicCombinedNested, expected: 'color: black;' }, + ].forEach(verifyStyleAttributeAppliedCorrectly); + + // class + [ + { cmp: dynamicClass, expected: 'class1' }, + { cmp: dynamicClassNested, expected: 'nestedClass1' }, + { cmp: dynamicCombined, expected: 'combinedClass' }, + { cmp: dynamicCombinedNested, expected: 'combinedClassNested' }, + ].forEach(verifyClassAppliedCorrectly); + + // attributes + [ + { cmp: dynamicAttr, expected: 'dynamic1' }, + { cmp: dynamicAttrNested, expected: 'dynamic2' }, + { cmp: dynamicCombined, expected: 'dynamic3' }, + { cmp: dynamicCombinedNested, expected: 'dynamic4' }, + ].forEach(verifyAttributeAppliedCorrectly); + }); + + it('updates values when expressions change', async () => { + const { + dynamicAttr, + dynamicStyle, + dynamicClass, + dynamicAttrNested, + dynamicStyleNested, + dynamicClassNested, + dynamicCombined, + dynamicCombinedNested, + } = nodes; + + // styles + + elm.dynamicStyle = 'color: teal;'; + elm.dynamicStyleNested = 'color: rose;'; + elm.combinedStyle = 'color: purple;'; + elm.combinedStyleNested = 'color: random;'; + + await Promise.resolve(); + + [ + { cmp: dynamicStyle, expected: 'color: teal;' }, + { cmp: dynamicStyleNested, expected: 'color: rose;' }, + { cmp: dynamicCombined, expected: 'color: purple;' }, + { cmp: dynamicCombinedNested, expected: 'color: random;' }, + ].forEach(verifyStyleAttributeAppliedCorrectly); + + // class + elm.dynamicClass = 'class2'; + elm.dynamicClassNested = 'nestedClass2'; + elm.combinedClass = 'combinedClassUpdated'; + elm.combinedClassNested = 'combinedClassNestedUpdated'; + + await Promise.resolve(); + + [ + { cmp: dynamicClass, expected: 'class2' }, + { cmp: dynamicClassNested, expected: 'nestedClass2' }, + { cmp: dynamicCombined, expected: 'combinedClassUpdated' }, + { cmp: dynamicCombinedNested, expected: 'combinedClassNestedUpdated' }, + ].forEach(verifyClassAppliedCorrectly); + + // attributes + elm.dynamicAttr = 'dynamicUpdated1'; + elm.dynamicAttrNested = 'dynamicUpdated2'; + elm.combinedAttr = 'dynamicUpdated3'; + elm.combinedAttrNested = 'dynamicUpdated4'; + + await Promise.resolve(); + + [ + { cmp: dynamicAttr, expected: 'dynamicUpdated1' }, + { cmp: dynamicAttrNested, expected: 'dynamicUpdated2' }, + { cmp: dynamicCombined, expected: 'dynamicUpdated3' }, + { cmp: dynamicCombinedNested, expected: 'dynamicUpdated4' }, + ].forEach(verifyAttributeAppliedCorrectly); + }); + + it('applies expression to deeply nested data structure', async () => { + const elm = createElement('x-deeply-nested', { is: DeepAttribute }); + document.body.appendChild(elm); + await Promise.resolve(); + + nodes = extractDataIds(elm); + + // Test includes 4 levels of depth + for (let i = 1; i < 5; i++) { + // style + [ + { cmp: nodes[`deep${i}Style`], expected: `${i}` }, + { cmp: nodes[`deep${i}StyleNested`], expected: `${i}` }, + ].forEach(verifyStyleAttributeAppliedCorrectly); + + // class + [ + { cmp: nodes[`deep${i}Class`], expected: `${i}` }, + { cmp: nodes[`deep${i}ClassNested`], expected: `${i}` }, + ].forEach(verifyClassAppliedCorrectly); + + // attribute + [ + { cmp: nodes[`deep${i}Attr`], expected: `${i}` }, + { cmp: nodes[`deep${i}AttrNested`], expected: `${i}` }, + ].forEach(verifyAttributeAppliedCorrectly); + + // combined + [ + { cmp: nodes[`deep${i}Combined`], expected: `${i}` }, + { cmp: nodes[`deep${i}CombinedNested`], expected: `${i}` }, + ].forEach(verifyAttributeAppliedCorrectly); + } + }); +}); + +describe('iframe onload event listener', () => { + it('works with iframe onload listener', async () => { + const elm = createElement('x-iframe-onload', { is: IframeOnload }); + document.body.appendChild(elm); + // Oddly Firefox requires two macrotasks before the load event fires. Chrome/Safari only require a microtask. + await new Promise((resolve) => setTimeout(resolve)); + await new Promise((resolve) => setTimeout(resolve)); + expect(elm.loaded).toBeTrue(); + }); +}); + +describe('key directive', () => { + it('works with a key directive on top-level static content', async () => { + const elm = createElement('x-with-key', { is: WithKey }); + document.body.appendChild(elm); + await Promise.resolve(); + const tbody = elm.shadowRoot.querySelector('tbody'); + expect(tbody.children.length).toBe(0); + + // one child + elm.items = [0]; + await Promise.resolve(); + expect(tbody.children.length).toBe(1); + const trsA = [...elm.shadowRoot.querySelectorAll('tr')]; + const tdsA = [...elm.shadowRoot.querySelectorAll('td')]; + expect(trsA.length).toBe(1); + expect(tdsA.length).toBe(1); + + // second child + elm.items = [0, 1]; + await Promise.resolve(); + expect(tbody.children.length).toBe(2); + const trsB = [...elm.shadowRoot.querySelectorAll('tr')]; + const tdsB = [...elm.shadowRoot.querySelectorAll('td')]; + + expect(trsB.length).toBe(2); + expect(tdsB.length).toBe(2); + expect(trsB[0]).toBe(trsA[0]); + expect(tdsB[0]).toBe(tdsA[0]); + + // switch order + elm.items = [1, 0]; + await Promise.resolve(); + expect(tbody.children.length).toBe(2); + const trsC = [...elm.shadowRoot.querySelectorAll('tr')]; + const tdsC = [...elm.shadowRoot.querySelectorAll('td')]; + + expect(trsC.length).toBe(2); + expect(tdsC.length).toBe(2); + expect(trsC[0]).toBe(trsB[1]); + expect(tdsC[0]).toBe(tdsB[1]); + expect(trsC[1]).toBe(trsB[0]); + expect(tdsC[1]).toBe(tdsB[0]); + }); +}); + +describe('static content dynamic text', () => { + it('renders expressions on mount', async () => { + const elm = createElement('x-text', { is: Text }); + document.body.appendChild(elm); + + await Promise.resolve(); + + const { emptyString, concateBeginning, concateEnd, siblings } = extractDataIds(elm); + + expect(emptyString.textContent).toEqual(''); + expect(concateBeginning.textContent).toEqual('default value'); + expect(concateEnd.textContent).toEqual('value default'); + + expect(siblings.childNodes.length).toBe(2); + expect(siblings.childNodes[0].textContent).toEqual('standard text'); + expect(siblings.childNodes[1].textContent).toEqual('second default'); + }); + + it('updates expressions on mount', async () => { + const elm = createElement('x-text', { is: Text }); + document.body.appendChild(elm); + + await Promise.resolve(); + + elm.emptyString = 'not empty'; + elm.dynamicText = 'updated'; + elm.siblingDynamicText = 'updated second'; + + await Promise.resolve(); + + const { emptyString, concateBeginning, concateEnd, siblings } = extractDataIds(elm); + + expect(emptyString.textContent).toEqual('not empty'); + expect(concateBeginning.textContent).toEqual('updated value'); + expect(concateEnd.textContent).toEqual('value updated'); + + expect(siblings.childNodes.length).toEqual(2); + expect(siblings.childNodes[0].textContent).toEqual('standard text'); + expect(siblings.childNodes[1].textContent).toEqual('updated second'); + }); +}); + +describe('table with static content containing expressions', () => { + it('renders static content correctly', async () => { + const table = createElement('x-table', { is: TableWithExpression }); + document.body.appendChild(table); + + await Promise.resolve(); + + const tbody = table.shadowRoot.querySelector('tbody'); + expect(tbody.children.length).toEqual(3); + const trs = [...table.shadowRoot.querySelectorAll('tr')]; + const tds = [...table.shadowRoot.querySelectorAll('td')]; + + expect(trs.length).toEqual(3); + expect(tds.length).toEqual(3); + + tds.forEach((td, i) => { + expect(td.getAttribute('class')).toEqual(`class${i}`); + expect(td.getAttribute('style')).toEqual(`color: ${i};`); + expect(td.getAttribute('data-id')).toEqual(`${i}`); + expect(td.textContent).toEqual(`value${i}`); + }); + }); +}); + +describe('text containing comments', () => { + [ + { + tagName: 'x-text-without-preserve-comments', + preserveComments: false, + ctor: TextWithoutPreserveComments, + expected: { + staticText: Array(4).fill('static text'), + initialDynamicText: Array(4).fill(' text'), + updatedDynamicText: Array(4).fill('dynamic text'), + initialMixedText: ' static text text text ', + updatedMixedText: ' static textmixed textmixed text ', + }, + }, + { + tagName: 'x-text-with-preserve-comments', + preserveComments: true, + ctor: TextWithPreserveComments, + expected: { + staticText: [ + 'static text', + 'static text', + 'static text', + 'static text', + ], + initialDynamicText: [ + ' text', + ' text', + ' text', + ' text', + ], + updatedDynamicText: [ + 'dynamic text', + 'dynamic text', + 'dynamic text', + 'dynamic text', + ], + initialMixedText: + ' static text text text ', + updatedMixedText: + ' static textmixed textmixed text ', + }, + }, + ].forEach(({ tagName, preserveComments, ctor, expected }) => { + describe(`preserveComments ${preserveComments}`, () => { + let elm; + let nodes; + beforeEach(async () => { + elm = createElement(tagName, { + is: ctor, + }); + document.body.appendChild(elm); + await Promise.resolve(); + nodes = extractDataIds(elm); + }); + + afterAll(() => { + elm.remove(); + }); + + const assertChildNodesInnerHTMLMatches = (actual, expected) => { + expect(actual.length).toEqual(expected.length); + // Actual is a HTMLCollection + expect(Array.from(actual).map((val) => val.innerHTML)).toEqual(expected); + }; + + it('renders static text correctly', () => { + const { staticText } = nodes; + assertChildNodesInnerHTMLMatches(staticText.children, expected.staticText); + }); + + it('renders dynamic text correctly', async () => { + const { dynamicText } = nodes; + // Initially empty variable + assertChildNodesInnerHTMLMatches(dynamicText.children, expected.initialDynamicText); + elm.dynamicText = 'dynamic'; + await Promise.resolve(); + assertChildNodesInnerHTMLMatches(dynamicText.children, expected.updatedDynamicText); + }); + + it('renders mixed static and dynamic text correctly', async () => { + const { mixedText } = nodes; + // Initially empty variable + expect(mixedText.innerHTML).toEqual(expected.initialMixedText); + elm.mixedText = 'mixed'; + await Promise.resolve(); + expect(mixedText.innerHTML).toEqual(expected.updatedMixedText); + }); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/attribute/attribute.html b/packages/@lwc/integration-not-karma/test/static-content/x/attribute/attribute.html new file mode 100644 index 0000000000..b615992eee --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/attribute/attribute.html @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/attribute/attribute.js b/packages/@lwc/integration-not-karma/test/static-content/x/attribute/attribute.js new file mode 100644 index 0000000000..1f9c0ee623 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/attribute/attribute.js @@ -0,0 +1,16 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api dynamicAttr = 'dynamic1'; + @api dynamicAttrNested = 'dynamic2'; + @api dynamicStyle = 'color: green;'; + @api dynamicStyleNested = 'color: violet;'; + @api dynamicClass = 'class1'; + @api dynamicClassNested = 'nestedClass1'; + @api combinedAttr = 'dynamic3'; + @api combinedStyle = 'color: orange;'; + @api combinedClass = 'combinedClass'; + @api combinedAttrNested = 'dynamic4'; + @api combinedStyleNested = 'color: black;'; + @api combinedClassNested = 'combinedClassNested'; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/comments/comments.html b/packages/@lwc/integration-not-karma/test/static-content/x/comments/comments.html new file mode 100644 index 0000000000..90577520df --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/comments/comments.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/comments/comments.js b/packages/@lwc/integration-not-karma/test/static-content/x/comments/comments.js new file mode 100644 index 0000000000..a5c838f25c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/comments/comments.js @@ -0,0 +1,22 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + fooWasClicked; + + @api + barWasClicked; + + onClickFoo() { + this.fooWasClicked = true; + } + + onClickBar() { + this.barWasClicked = true; + } + + @api + getRefs() { + return this.refs; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/component/component.css b/packages/@lwc/integration-not-karma/test/static-content/x/component/component.css new file mode 100644 index 0000000000..5b5436e874 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/component/component.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/component/component.html b/packages/@lwc/integration-not-karma/test/static-content/x/component/component.html new file mode 100644 index 0000000000..e50da2b875 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/component/component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/component/component.js b/packages/@lwc/integration-not-karma/test/static-content/x/component/component.js new file mode 100644 index 0000000000..6d3542bb2f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Component extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/container/container.html b/packages/@lwc/integration-not-karma/test/static-content/x/container/container.html new file mode 100644 index 0000000000..4e179eb39d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/container/container.html @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/container/container.js b/packages/@lwc/integration-not-karma/test/static-content/x/container/container.js new file mode 100644 index 0000000000..5da038afe9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/container/container.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Container extends LightningElement { + @api syntheticFirst = false; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/deepAttribute/deepAttribute.html b/packages/@lwc/integration-not-karma/test/static-content/x/deepAttribute/deepAttribute.html new file mode 100644 index 0000000000..a60ea0f985 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/deepAttribute/deepAttribute.html @@ -0,0 +1,49 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/deepAttribute/deepAttribute.js b/packages/@lwc/integration-not-karma/test/static-content/x/deepAttribute/deepAttribute.js new file mode 100644 index 0000000000..d3e5e380c8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/deepAttribute/deepAttribute.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + one = '1'; + un = { deux: '2' }; + uno = { dos: { tres: '3' } }; + ichi = { ni: { san: { shi: '4' } } }; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/deepListener/deepListener.html b/packages/@lwc/integration-not-karma/test/static-content/x/deepListener/deepListener.html new file mode 100644 index 0000000000..3d13762f1d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/deepListener/deepListener.html @@ -0,0 +1,29 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/deepListener/deepListener.js b/packages/@lwc/integration-not-karma/test/static-content/x/deepListener/deepListener.js new file mode 100644 index 0000000000..bc35fe60a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/deepListener/deepListener.js @@ -0,0 +1,33 @@ +import { LightningElement, api } from 'lwc'; + +export default class App extends LightningElement { + @api counter = 0; + + one() { + this.counter++; + } + + un = { + deux: () => { + this.counter++; + }, + }; + + uno = { + dos: { + tres: () => { + this.counter++; + }, + }, + }; + + ichi = { + ni: { + san: { + shi: () => { + this.counter++; + }, + }, + }, + }; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/escape/escape.html b/packages/@lwc/integration-not-karma/test/static-content/x/escape/escape.html new file mode 100644 index 0000000000..cf40c868f6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/escape/escape.html @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/escape/escape.js b/packages/@lwc/integration-not-karma/test/static-content/x/escape/escape.js new file mode 100644 index 0000000000..2ca7708e5b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/escape/escape.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Escape extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/iframeOnload/iframeOnload.html b/packages/@lwc/integration-not-karma/test/static-content/x/iframeOnload/iframeOnload.html new file mode 100644 index 0000000000..7edc126067 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/iframeOnload/iframeOnload.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/iframeOnload/iframeOnload.js b/packages/@lwc/integration-not-karma/test/static-content/x/iframeOnload/iframeOnload.js new file mode 100644 index 0000000000..096f054523 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/iframeOnload/iframeOnload.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api loaded = false; + + loadHandler() { + this.loaded = true; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/listenerStaticWithUpdates/listenerStaticWithUpdates.html b/packages/@lwc/integration-not-karma/test/static-content/x/listenerStaticWithUpdates/listenerStaticWithUpdates.html new file mode 100644 index 0000000000..6917e5dc87 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/listenerStaticWithUpdates/listenerStaticWithUpdates.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/listenerStaticWithUpdates/listenerStaticWithUpdates.js b/packages/@lwc/integration-not-karma/test/static-content/x/listenerStaticWithUpdates/listenerStaticWithUpdates.js new file mode 100644 index 0000000000..ab00870700 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/listenerStaticWithUpdates/listenerStaticWithUpdates.js @@ -0,0 +1,18 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + fooEventCount = 0; + + @api + version = 0; + + handleFooEvent() { + this.fooEventCount++; + } + + @api + fireFooEvent() { + this.template.querySelector('div').dispatchEvent(new CustomEvent('foo')); + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/a.css b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/a.css new file mode 100644 index 0000000000..5b5436e874 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/a.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/a.html b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/a.html new file mode 100644 index 0000000000..37e4160cf8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/a.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/b.html b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/b.html new file mode 100644 index 0000000000..1c210f7bc7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/b.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/b.scoped.css b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/b.scoped.css new file mode 100644 index 0000000000..cd16730bd8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/b.scoped.css @@ -0,0 +1,3 @@ +div { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/multipleStyles.js b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/multipleStyles.js new file mode 100644 index 0000000000..3e1672fbe8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/multipleStyles/multipleStyles.js @@ -0,0 +1,27 @@ +import { LightningElement, api } from 'lwc'; +import aTemplate from './a.html'; +import bTemplate from './b.html'; +import aCss from './a.css'; +import bCss from './b.scoped.css?scoped=true'; + +const templateMap = { + a: aTemplate, + b: bTemplate, +}; +export default class Container extends LightningElement { + _template = aTemplate; + + @api + updateTemplate({ name, useScopedCss }) { + const template = templateMap[name]; + + // TODO [#2826]: freeze the template object and stop supporting setting the stylesheets + template.stylesheets = useScopedCss ? [...aCss, ...bCss] : [...aCss]; + + this._template = template; + } + + render() { + return this._template; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/native/native.html b/packages/@lwc/integration-not-karma/test/static-content/x/native/native.html new file mode 100644 index 0000000000..3db48ef294 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/native/native.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/native/native.js b/packages/@lwc/integration-not-karma/test/static-content/x/native/native.js new file mode 100644 index 0000000000..9397b74759 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/native/native.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class Native extends LightningElement { + static shadowSupportMode = 'native'; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListener/onlyEventListener.html b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListener/onlyEventListener.html new file mode 100644 index 0000000000..74d50f8efc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListener/onlyEventListener.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListener/onlyEventListener.js b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListener/onlyEventListener.js new file mode 100644 index 0000000000..b855a2919a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListener/onlyEventListener.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api counts = {}; + @api dynamic = ''; + + onFoo() { + this.counts.foo = (this.counts.foo || 0) + 1; + } + + onBar() { + this.counts.bar = (this.counts.bar || 0) + 1; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerChild/onlyEventListenerChild.html b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerChild/onlyEventListenerChild.html new file mode 100644 index 0000000000..496337b81b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerChild/onlyEventListenerChild.html @@ -0,0 +1,9 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerChild/onlyEventListenerChild.js b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerChild/onlyEventListenerChild.js new file mode 100644 index 0000000000..b855a2919a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerChild/onlyEventListenerChild.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api counts = {}; + @api dynamic = ''; + + onFoo() { + this.counts.foo = (this.counts.foo || 0) + 1; + } + + onBar() { + this.counts.bar = (this.counts.bar || 0) + 1; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerGrandchild/onlyEventListenerGrandchild.html b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerGrandchild/onlyEventListenerGrandchild.html new file mode 100644 index 0000000000..9751a248f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerGrandchild/onlyEventListenerGrandchild.html @@ -0,0 +1,11 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerGrandchild/onlyEventListenerGrandchild.js b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerGrandchild/onlyEventListenerGrandchild.js new file mode 100644 index 0000000000..b855a2919a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/onlyEventListenerGrandchild/onlyEventListenerGrandchild.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api counts = {}; + @api dynamic = ''; + + onFoo() { + this.counts.foo = (this.counts.foo || 0) + 1; + } + + onBar() { + this.counts.bar = (this.counts.bar || 0) + 1; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/preserveComments/preserveComments.html b/packages/@lwc/integration-not-karma/test/static-content/x/preserveComments/preserveComments.html new file mode 100644 index 0000000000..3e43c666b1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/preserveComments/preserveComments.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/preserveComments/preserveComments.js b/packages/@lwc/integration-not-karma/test/static-content/x/preserveComments/preserveComments.js new file mode 100644 index 0000000000..a5c838f25c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/preserveComments/preserveComments.js @@ -0,0 +1,22 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + fooWasClicked; + + @api + barWasClicked; + + onClickFoo() { + this.fooWasClicked = true; + } + + onClickBar() { + this.barWasClicked = true; + } + + @api + getRefs() { + return this.refs; + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/staticUnsafeTopLevel/staticUnsafeTopLevel.html b/packages/@lwc/integration-not-karma/test/static-content/x/staticUnsafeTopLevel/staticUnsafeTopLevel.html new file mode 100644 index 0000000000..bc8518b18c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/staticUnsafeTopLevel/staticUnsafeTopLevel.html @@ -0,0 +1,30 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/staticUnsafeTopLevel/staticUnsafeTopLevel.js b/packages/@lwc/integration-not-karma/test/static-content/x/staticUnsafeTopLevel/staticUnsafeTopLevel.js new file mode 100644 index 0000000000..9e510127fb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/staticUnsafeTopLevel/staticUnsafeTopLevel.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api doRender = false; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgNs/svgNs.html b/packages/@lwc/integration-not-karma/test/static-content/x/svgNs/svgNs.html new file mode 100644 index 0000000000..381aaaf39b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgNs/svgNs.html @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgNs/svgNs.js b/packages/@lwc/integration-not-karma/test/static-content/x/svgNs/svgNs.js new file mode 100644 index 0000000000..f426960211 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgNs/svgNs.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class SvgNs extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgPath/svgPath.html b/packages/@lwc/integration-not-karma/test/static-content/x/svgPath/svgPath.html new file mode 100644 index 0000000000..d74c88cee4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgPath/svgPath.html @@ -0,0 +1,6 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgPath/svgPath.js b/packages/@lwc/integration-not-karma/test/static-content/x/svgPath/svgPath.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgPath/svgPath.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInDiv/svgPathInDiv.html b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInDiv/svgPathInDiv.html new file mode 100644 index 0000000000..9a59b19538 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInDiv/svgPathInDiv.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInDiv/svgPathInDiv.js b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInDiv/svgPathInDiv.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInDiv/svgPathInDiv.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInG/svgPathInG.html b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInG/svgPathInG.html new file mode 100644 index 0000000000..d2e1ff6be9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInG/svgPathInG.html @@ -0,0 +1,8 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInG/svgPathInG.js b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInG/svgPathInG.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/svgPathInG/svgPathInG.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/table/table.html b/packages/@lwc/integration-not-karma/test/static-content/x/table/table.html new file mode 100644 index 0000000000..6ff6fb83e7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/table/table.html @@ -0,0 +1,11 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/table/table.js b/packages/@lwc/integration-not-karma/test/static-content/x/table/table.js new file mode 100644 index 0000000000..4310425be5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/table/table.js @@ -0,0 +1,13 @@ +import { LightningElement, api, track } from 'lwc'; + +let count = 0; + +export default class extends LightningElement { + @track + rows = []; + + @api + addRow() { + this.rows.push(++count); + } +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/tableWithExpressions/tableWithExpressions.html b/packages/@lwc/integration-not-karma/test/static-content/x/tableWithExpressions/tableWithExpressions.html new file mode 100644 index 0000000000..ce11e98192 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/tableWithExpressions/tableWithExpressions.html @@ -0,0 +1,11 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/tableWithExpressions/tableWithExpressions.js b/packages/@lwc/integration-not-karma/test/static-content/x/tableWithExpressions/tableWithExpressions.js new file mode 100644 index 0000000000..342d0d9db5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/tableWithExpressions/tableWithExpressions.js @@ -0,0 +1,24 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + rows = [ + { + id: 0, + style: 'color: 0;', + class: 'class0', + value: 'value0', + }, + { + id: 1, + style: 'color: 1;', + class: 'class1', + value: 'value1', + }, + { + id: 2, + style: 'color: 2;', + class: 'class2', + value: 'value2', + }, + ]; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/text/text.html b/packages/@lwc/integration-not-karma/test/static-content/x/text/text.html new file mode 100644 index 0000000000..e11eaec371 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/text/text.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/text/text.js b/packages/@lwc/integration-not-karma/test/static-content/x/text/text.js new file mode 100644 index 0000000000..87e9f97965 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/text/text.js @@ -0,0 +1,7 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api emptyString = ''; + @api dynamicText = 'default'; + @api siblingDynamicText = 'second default'; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/textWithPreserveComments/textWithPreserveComments.html b/packages/@lwc/integration-not-karma/test/static-content/x/textWithPreserveComments/textWithPreserveComments.html new file mode 100644 index 0000000000..0be6820869 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/textWithPreserveComments/textWithPreserveComments.html @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/textWithPreserveComments/textWithPreserveComments.js b/packages/@lwc/integration-not-karma/test/static-content/x/textWithPreserveComments/textWithPreserveComments.js new file mode 100644 index 0000000000..8961f3900d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/textWithPreserveComments/textWithPreserveComments.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api dynamicText; + @api mixedText; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/textWithoutPreserveComments/textWithoutPreserveComments.html b/packages/@lwc/integration-not-karma/test/static-content/x/textWithoutPreserveComments/textWithoutPreserveComments.html new file mode 100644 index 0000000000..8db8bb598b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/textWithoutPreserveComments/textWithoutPreserveComments.html @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/textWithoutPreserveComments/textWithoutPreserveComments.js b/packages/@lwc/integration-not-karma/test/static-content/x/textWithoutPreserveComments/textWithoutPreserveComments.js new file mode 100644 index 0000000000..8961f3900d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/textWithoutPreserveComments/textWithoutPreserveComments.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api dynamicText; + @api mixedText; +} diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/withKey/withKey.html b/packages/@lwc/integration-not-karma/test/static-content/x/withKey/withKey.html new file mode 100644 index 0000000000..6f1619467b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/withKey/withKey.html @@ -0,0 +1,11 @@ + diff --git a/packages/@lwc/integration-not-karma/test/static-content/x/withKey/withKey.js b/packages/@lwc/integration-not-karma/test/static-content/x/withKey/withKey.js new file mode 100644 index 0000000000..503059cb83 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/x/withKey/withKey.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api items = []; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/a/a.html b/packages/@lwc/integration-not-karma/test/swapping/components/base/a/a.html new file mode 100644 index 0000000000..035c1d3d32 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/a/a.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/a/a.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/a/a.js new file mode 100644 index 0000000000..2550bd693e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/a/a.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class A extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/b/b.html b/packages/@lwc/integration-not-karma/test/swapping/components/base/b/b.html new file mode 100644 index 0000000000..53bd907e14 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/b/b.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/b/b.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/b/b.js new file mode 100644 index 0000000000..f93991df85 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/b/b.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class B extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/c/c.html b/packages/@lwc/integration-not-karma/test/swapping/components/base/c/c.html new file mode 100644 index 0000000000..d9647d362d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/c/c.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/c/c.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/c/c.js new file mode 100644 index 0000000000..a580373312 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/c/c.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class C extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/container/container.html b/packages/@lwc/integration-not-karma/test/swapping/components/base/container/container.html new file mode 100644 index 0000000000..41da6cc12a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/container/container.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/container/container.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/container/container.js new file mode 100644 index 0000000000..22045a5f17 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/container/container.js @@ -0,0 +1,10 @@ +import { LightningElement, api } from 'lwc'; +import Z from 'base/libraryz'; + +export default class Container extends LightningElement { + @api + testValue; + connectedCallback() { + this.testValue = new Z().value; + } +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/d/d.html b/packages/@lwc/integration-not-karma/test/swapping/components/base/d/d.html new file mode 100644 index 0000000000..0abdabc49d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/d/d.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/d/d.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/d/d.js new file mode 100644 index 0000000000..1f8be71c10 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/d/d.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class D extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/e/e.html b/packages/@lwc/integration-not-karma/test/swapping/components/base/e/e.html new file mode 100644 index 0000000000..c4b1342e48 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/e/e.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/e/e.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/e/e.js new file mode 100644 index 0000000000..d6c81e9d86 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/e/e.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class E extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/libraryx/libraryx.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/libraryx/libraryx.js new file mode 100644 index 0000000000..feba9337e7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/libraryx/libraryx.js @@ -0,0 +1,5 @@ +export default class LibraryX { + constructor() { + this.value = 'I am not a component'; + } +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/base/libraryz/libraryz.js b/packages/@lwc/integration-not-karma/test/swapping/components/base/libraryz/libraryz.js new file mode 100644 index 0000000000..3b8d980f79 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/base/libraryz/libraryz.js @@ -0,0 +1,5 @@ +export default class LibraryZ { + constructor() { + this.value = 'I may look like a component'; + } +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/components/index.spec.js b/packages/@lwc/integration-not-karma/test/swapping/components/index.spec.js new file mode 100644 index 0000000000..a3a542083b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/components/index.spec.js @@ -0,0 +1,62 @@ +import { createElement, swapComponent } from 'lwc'; + +import Container from 'base/container'; +import A from 'base/a'; +import B from 'base/b'; +import C from 'base/c'; +import D from 'base/d'; +import E from 'base/e'; +import X from 'base/libraryx'; +import Z from 'base/libraryz'; + +// Swapping is only enabled in dev mode +describe.skipIf(process.env.NODE_ENV === 'production')('component swapping', () => { + it('should work before and after instantiation', () => { + expect(swapComponent(A, B)).toBe(true); + const elm = createElement('x-container', { is: Container }); + document.body.appendChild(elm); + expect(elm.shadowRoot.firstChild.shadowRoot.firstChild.outerHTML).toBe( + '

        b

        ' + ); + expect(swapComponent(B, C)).toBe(true); + return Promise.resolve().then(() => { + expect(elm.shadowRoot.firstChild.shadowRoot.firstChild.outerHTML).toBe( + '

        c

        ' + ); + }); + }); + + it('should return false for root elements', () => { + const elm = createElement('x-d', { is: D }); + document.body.appendChild(elm); + expect(swapComponent(D, E)).toBe(false); // meaning you can reload the page + }); + + it('should throw for invalid old component', () => { + expect(() => { + swapComponent(function () {}, D); + }).toThrowError( + TypeError, + /Invalid Component: Attempting to swap a non-component with a component/ + ); + }); + + it('should throw for invalid new componeont', () => { + expect(() => { + swapComponent(D, function () {}); + }).toThrowError( + TypeError, + /Invalid Component: Attempting to swap a component with a non-component/ + ); + }); + + it('should be a no-op for non components', () => { + const elm = createElement('x-container', { is: Container }); + document.body.appendChild(elm); + expect(elm.testValue).toBe('I may look like a component'); + expect(swapComponent(Z, X)).toBe(false); + return Promise.resolve().then(() => { + expect(elm.testValue).toBe('I may look like a component'); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/index.spec.js b/packages/@lwc/integration-not-karma/test/swapping/styles/index.spec.js new file mode 100644 index 0000000000..1da1911952 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/index.spec.js @@ -0,0 +1,321 @@ +import { createElement, swapStyle, swapTemplate } from 'lwc'; +import { extractDataIds } from 'test-utils'; +import ShadowUsesStaticStylesheets from 'shadow/usesStaticStylesheets'; +import LightUsesStaticStylesheets from 'light/usesStaticStylesheets'; +import LightGlobalUsesStaticStylesheets from 'light-global/usesStaticStylesheets'; +import ShadowSimple from 'shadow/simple'; +import ShadowStaleProp from 'shadow/staleProp'; +import LightSimple from 'light/simple'; +import LightStaleProp from 'light/staleProp'; +import LightGlobalSimple from 'light-global/simple'; +import LightGlobalStaleProp from 'light-global/staleProp'; +import LibraryUserA from 'x/libraryUserA'; +import LibraryUserB from 'x/libraryUserB'; +import libraryStyle from 'x/library'; +import libraryStyleV2 from 'x/libraryV2'; +import IdenticalStylesheets from 'shadow/identicalStylesheets'; +import IdenticalStylesheetsContainer from 'shadow/identicalStylesheetsContainer'; + +function expectStyles(elm, styles) { + const computed = getComputedStyle(elm); + for (const [style, value] of Object.entries(styles)) { + expect(computed[style]).toBe(value); + } +} + +// Swapping is only enabled in dev mode +describe.skipIf(process.env.NODE_ENV === 'production')('style swapping', () => { + afterEach(() => { + window.__lwcResetHotSwaps(); + window.__lwcResetStylesheetCache(); + window.__lwcResetGlobalStylesheets(); + }); + + const scenarios = [ + { + testName: 'shadow', + components: { + Simple: ShadowSimple, + StaleProp: ShadowStaleProp, + UsesStaticStylesheets: ShadowUsesStaticStylesheets, + }, + }, + { + testName: 'light', + components: { + Simple: LightSimple, + StaleProp: LightStaleProp, + UsesStaticStylesheets: LightUsesStaticStylesheets, + }, + }, + { + testName: 'light-global', + components: { + Simple: LightGlobalSimple, + StaleProp: LightGlobalStaleProp, + UsesStaticStylesheets: LightGlobalUsesStaticStylesheets, + }, + }, + ]; + scenarios.forEach(({ testName, components }) => { + describe(testName, () => { + const { Simple, StaleProp, UsesStaticStylesheets } = components; + it('should work with components with implicit style definition', async () => { + const { blockStyle, inlineStyle, noneStyle } = Simple; + const elm = createElement(`${testName}-simple`, { is: Simple }); + document.body.appendChild(elm); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + }); + swapStyle(blockStyle[0], inlineStyle[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'inline', + }); + swapStyle(inlineStyle[0], noneStyle[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'none', + }); + }); + + it('should remove stale prop', async () => { + const { stylesV1, stylesV2, stylesV3 } = StaleProp; + const elm = createElement(`${testName}-stale-prop`, { is: StaleProp }); + document.body.appendChild(elm); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'flex', + opacity: '1', + borderRadius: '0px', + }); + swapStyle(stylesV1[0], stylesV2[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + opacity: '0.5', + borderRadius: '0px', + }); + swapStyle(stylesV2[0], stylesV3[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + opacity: '1', + borderRadius: '5px', + }); + }); + + it('should remove stale prop while swapping back and forth', async () => { + const { stylesV1, stylesV2 } = StaleProp; + const elm = createElement(`${testName}-stale-prop`, { is: StaleProp }); + document.body.appendChild(elm); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'flex', + opacity: '1', + }); + swapStyle(stylesV1[0], stylesV2[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + opacity: '0.5', + }); + swapStyle(stylesV2[0], stylesV1[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'flex', + opacity: '1', + }); + }); + + it('should replace the same stylesheet in multiple components', async () => { + const { stylesV1, stylesV2 } = StaleProp; + const elm1 = createElement(`${testName}-stale-prop`, { is: StaleProp }); + const elm2 = createElement(`${testName}-stale-prop`, { is: StaleProp }); + document.body.appendChild(elm1); + document.body.appendChild(elm2); + + await Promise.resolve(); + for (const elm of [elm1, elm2]) { + expectStyles(extractDataIds(elm).paragraph, { + display: 'flex', + opacity: '1', + }); + } + swapStyle(stylesV1[0], stylesV2[0]); + + await Promise.resolve(); + for (const elm of [elm1, elm2]) { + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + opacity: '0.5', + }); + } + }); + + it('should replace static stylesheets', async () => { + const { asStatic, asStaticV2 } = UsesStaticStylesheets; + const elm = createElement(`${testName}-uses-static-stylesheets`, { + is: UsesStaticStylesheets, + }); + document.body.appendChild(elm); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + color: 'rgba(255, 0, 0, 0)', + fontStyle: 'italic', + }); + + swapStyle(asStatic[0], asStaticV2[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + color: 'rgba(0, 0, 255, 0)', + fontStyle: 'italic', + }); + }); + + it('should be able to swap a style that will be used in future', async () => { + const { blockStyle, inlineStyle, noneStyle } = Simple; + const elm = createElement(`${testName}-simple`, { is: Simple }); + document.body.appendChild(elm); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + }); + // Swap inlineStyle that hasn't been rendered yet + swapStyle(inlineStyle[0], noneStyle[0]); + + await Promise.resolve(); + // Verify that rendered content did not change + expectStyles(extractDataIds(elm).paragraph, { + display: 'block', + }); + // Swap blockStyle to inlineStyle, which will transitively be swapped to noneStyle + swapStyle(blockStyle[0], inlineStyle[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'none', + }); + }); + }); + }); + + it('should be able to swap stylesheets that produce identical content', async () => { + const { style, identicalStyle, newStyle, implicitTemplate, newTemplate } = + IdenticalStylesheets; + const elm = createElement(`identical-stylesheets`, { + is: IdenticalStylesheetsContainer, + }); + document.body.appendChild(elm); + + // Step 1: wait for first render + // implicitTemplate is associated with identicalStyle + await Promise.resolve(); + expectStyles(extractDataIds(elm).paragraph, { + display: 'inline', + }); + + // Step 2: swap template with a new template to trigger rehydration + swapTemplate(implicitTemplate, newTemplate); + await Promise.resolve(); + + // Step 3: Associate an identical stylesheet with the same template(or a template with the same shadow token) + // associate identical stylesheet with the original template + expect(() => { + implicitTemplate.stylesheets = style; + }).toLogWarningDev(/Mutating the "stylesheets" property on a template/); + // reswap the template to implicit template + swapTemplate(newTemplate, implicitTemplate); + await Promise.resolve(); + + // Act to trigger the unrender of the identical stylesheets + swapStyle(style[0], newStyle[0]); + swapStyle(identicalStyle[0], newStyle[0]); + await Promise.resolve(); + + // Assert that the swap is successful + expectStyles(extractDataIds(elm).paragraph, { + display: 'none', + }); + }); + + describe('CSS library', () => { + const { style: styleA, styleV2: styleAV2 } = LibraryUserA; + const { style: styleB, styleV2: styleBV2 } = LibraryUserB; + + let elmA; + let elmB; + + beforeEach(async () => { + elmA = createElement('x-library-user-a', { is: LibraryUserA }); + elmB = createElement(`x-library-user-b`, { is: LibraryUserB }); + document.body.appendChild(elmA); + document.body.appendChild(elmB); + + await Promise.resolve(); + expectStyles(extractDataIds(elmA).paragraph, { + fontSize: '10px', + fontWeight: '100', + }); + expectStyles(extractDataIds(elmB).paragraph, { + fontSize: '10px', + fontWeight: '800', + }); + }); + + it('swaps a library CSS file', async () => { + swapStyle(libraryStyle[0], libraryStyleV2[0]); + + await Promise.resolve(); + expectStyles(extractDataIds(elmA).paragraph, { + fontSize: '20px', + fontWeight: '100', + }); + expectStyles(extractDataIds(elmB).paragraph, { + fontSize: '20px', + fontWeight: '800', + }); + }); + + it('swaps a non-library CSS file while keeping library styles', async () => { + // The library (`@import`) is the first stylesheet, so grab the second instead + swapStyle(styleA[1], styleAV2[1]); + + await Promise.resolve(); + expectStyles(extractDataIds(elmA).paragraph, { + fontSize: '10px', + fontWeight: '200', + }); + expectStyles(extractDataIds(elmB).paragraph, { + fontSize: '10px', + fontWeight: '800', + }); + + // The library (`@import`) is the first stylesheet, so grab the second instead + swapStyle(styleB[1], styleBV2[1]); + + await Promise.resolve(); + expectStyles(extractDataIds(elmA).paragraph, { + fontSize: '10px', + fontWeight: '200', + }); + expectStyles(extractDataIds(elmB).paragraph, { + fontSize: '10px', + fontWeight: '900', + }); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/inline.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/inline.css new file mode 100644 index 0000000000..b2397d4adc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/inline.css @@ -0,0 +1,3 @@ +.simple { + display: inline; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/none.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/none.css new file mode 100644 index 0000000000..c9c92b6439 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/none.css @@ -0,0 +1,3 @@ +.simple { + display: none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.css new file mode 100644 index 0000000000..98dd3602de --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.css @@ -0,0 +1,3 @@ +.simple { + display: block; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.html b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.html new file mode 100644 index 0000000000..2c2eb91e51 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.js b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.js new file mode 100644 index 0000000000..b54178cd72 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/simple/simple.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; +import block from './simple.css'; +import inline from './inline.css'; +import none from './none.css'; + +export default class Simple extends LightningElement { + static renderMode = 'light'; + static blockStyle = block; + static inlineStyle = inline; + static noneStyle = none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.css new file mode 100644 index 0000000000..657178ffe6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.css @@ -0,0 +1,3 @@ +p { + display: flex; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.html b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.html new file mode 100644 index 0000000000..fac65630e6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.js b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.js new file mode 100644 index 0000000000..b7cbb21958 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/staleProp.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; +import stylesV1 from './staleProp.css'; +import stylesV2 from './stylesV2.css'; +import stylesV3 from './stylesV3.css'; + +export default class extends LightningElement { + static renderMode = 'light'; + static stylesV1 = stylesV1; + static stylesV2 = stylesV2; + static stylesV3 = stylesV3; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/stylesV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/stylesV2.css new file mode 100644 index 0000000000..987d26e79d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/stylesV2.css @@ -0,0 +1,3 @@ +p { + opacity: 0.5; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/stylesV3.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/stylesV3.css new file mode 100644 index 0000000000..b61206b466 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/staleProp/stylesV3.css @@ -0,0 +1,3 @@ +p { + border-radius: 5px; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/asStatic.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/asStatic.css new file mode 100644 index 0000000000..ba894d0161 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/asStatic.css @@ -0,0 +1,3 @@ +p { + color: rgba(255, 0, 0, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/asStaticV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/asStaticV2.css new file mode 100644 index 0000000000..eeae35d47f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/asStaticV2.css @@ -0,0 +1,3 @@ +p { + color: rgba(0, 0, 255, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.css new file mode 100644 index 0000000000..1e375b2148 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.css @@ -0,0 +1,3 @@ +p { + font-style: italic; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.html b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.html new file mode 100644 index 0000000000..96ea4b82f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.js b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.js new file mode 100644 index 0000000000..2fd93f2031 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light-global/usesStaticStylesheets/usesStaticStylesheets.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; +import asStatic from './asStatic.css'; +import asStaticV2 from './asStaticV2.css'; + +export default class extends LightningElement { + static renderMode = 'light'; + static stylesheets = [asStatic]; + + static asStatic = asStatic; + static asStaticV2 = asStaticV2; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/inline.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/inline.scoped.css new file mode 100644 index 0000000000..b2397d4adc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/inline.scoped.css @@ -0,0 +1,3 @@ +.simple { + display: inline; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/none.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/none.scoped.css new file mode 100644 index 0000000000..c9c92b6439 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/none.scoped.css @@ -0,0 +1,3 @@ +.simple { + display: none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.html b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.html new file mode 100644 index 0000000000..2c2eb91e51 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.js b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.js new file mode 100644 index 0000000000..4f50fbadcd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; +import block from './simple.scoped.css'; +import inline from './inline.scoped.css'; +import none from './none.scoped.css'; + +export default class Simple extends LightningElement { + static renderMode = 'light'; + static blockStyle = block; + static inlineStyle = inline; + static noneStyle = none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.scoped.css new file mode 100644 index 0000000000..98dd3602de --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/simple/simple.scoped.css @@ -0,0 +1,3 @@ +.simple { + display: block; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.html b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.html new file mode 100644 index 0000000000..fac65630e6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.js b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.js new file mode 100644 index 0000000000..b2ee8f99f3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; +import stylesV1 from './staleProp.scoped.css'; +import stylesV2 from './stylesV2.scoped.css'; +import stylesV3 from './stylesV3.scoped.css'; + +export default class extends LightningElement { + static renderMode = 'light'; + static stylesV1 = stylesV1; + static stylesV2 = stylesV2; + static stylesV3 = stylesV3; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.scoped.css new file mode 100644 index 0000000000..657178ffe6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/staleProp.scoped.css @@ -0,0 +1,3 @@ +p { + display: flex; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/stylesV2.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/stylesV2.scoped.css new file mode 100644 index 0000000000..987d26e79d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/stylesV2.scoped.css @@ -0,0 +1,3 @@ +p { + opacity: 0.5; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/stylesV3.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/stylesV3.scoped.css new file mode 100644 index 0000000000..b61206b466 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/staleProp/stylesV3.scoped.css @@ -0,0 +1,3 @@ +p { + border-radius: 5px; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/asStatic.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/asStatic.scoped.css new file mode 100644 index 0000000000..ba894d0161 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/asStatic.scoped.css @@ -0,0 +1,3 @@ +p { + color: rgba(255, 0, 0, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/asStaticV2.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/asStaticV2.scoped.css new file mode 100644 index 0000000000..eeae35d47f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/asStaticV2.scoped.css @@ -0,0 +1,3 @@ +p { + color: rgba(0, 0, 255, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.html b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.html new file mode 100644 index 0000000000..96ea4b82f4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.js b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.js new file mode 100644 index 0000000000..62d1c85f41 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; +import asStatic from './asStatic.scoped.css'; +import asStaticV2 from './asStaticV2.scoped.css'; + +export default class extends LightningElement { + static renderMode = 'light'; + static stylesheets = [asStatic]; + + static asStatic = asStatic; + static asStaticV2 = asStaticV2; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.scoped.css b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.scoped.css new file mode 100644 index 0000000000..1e375b2148 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/light/usesStaticStylesheets/usesStaticStylesheets.scoped.css @@ -0,0 +1,3 @@ +p { + font-style: italic; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.css new file mode 100644 index 0000000000..dc7d638fdd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.css @@ -0,0 +1,3 @@ +.identical { + display: inline; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.html b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.html new file mode 100644 index 0000000000..35d5616ac9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.js b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.js new file mode 100644 index 0000000000..a55bb258a2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/identicalStylesheets.js @@ -0,0 +1,14 @@ +import { LightningElement } from 'lwc'; +import style from './style.css'; +import identicalStyle from './identicalStylesheets.css'; +import newStyle from './newStyle.css'; +import implicitTemplate from './identicalStylesheets.html'; +import newTemplate from './newTemplate.html'; + +export default class IdenticalStylesheet extends LightningElement { + static style = style; + static identicalStyle = identicalStyle; + static newStyle = newStyle; + static implicitTemplate = implicitTemplate; + static newTemplate = newTemplate; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/newStyle.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/newStyle.css new file mode 100644 index 0000000000..8dc37d4376 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/newStyle.css @@ -0,0 +1,3 @@ +.identical { + display: none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/newTemplate.html b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/newTemplate.html new file mode 100644 index 0000000000..6f01a53f17 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/newTemplate.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/style.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/style.css new file mode 100644 index 0000000000..dc7d638fdd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheets/style.css @@ -0,0 +1,3 @@ +.identical { + display: inline; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheetsContainer/identicalStylesheetsContainer.html b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheetsContainer/identicalStylesheetsContainer.html new file mode 100644 index 0000000000..4896a0dbc6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheetsContainer/identicalStylesheetsContainer.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheetsContainer/identicalStylesheetsContainer.js b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheetsContainer/identicalStylesheetsContainer.js new file mode 100644 index 0000000000..21a45d50a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/identicalStylesheetsContainer/identicalStylesheetsContainer.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Container extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/inline.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/inline.css new file mode 100644 index 0000000000..b2397d4adc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/inline.css @@ -0,0 +1,3 @@ +.simple { + display: inline; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/none.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/none.css new file mode 100644 index 0000000000..c9c92b6439 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/none.css @@ -0,0 +1,3 @@ +.simple { + display: none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.css new file mode 100644 index 0000000000..98dd3602de --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.css @@ -0,0 +1,3 @@ +.simple { + display: block; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.html b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.html new file mode 100644 index 0000000000..97b369db93 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.js b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.js new file mode 100644 index 0000000000..2138f47cfc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/simple/simple.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; +import block from './simple.css'; +import inline from './inline.css'; +import none from './none.css'; + +export default class Simple extends LightningElement { + static blockStyle = block; + static inlineStyle = inline; + static noneStyle = none; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.css new file mode 100644 index 0000000000..657178ffe6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.css @@ -0,0 +1,3 @@ +p { + display: flex; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.html b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.html new file mode 100644 index 0000000000..bf9b8dfd61 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.js b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.js new file mode 100644 index 0000000000..a408931349 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/staleProp.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; +import stylesV1 from './staleProp.css'; +import stylesV2 from './stylesV2.css'; +import stylesV3 from './stylesV3.css'; + +export default class extends LightningElement { + static stylesV1 = stylesV1; + static stylesV2 = stylesV2; + static stylesV3 = stylesV3; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/stylesV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/stylesV2.css new file mode 100644 index 0000000000..987d26e79d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/stylesV2.css @@ -0,0 +1,3 @@ +p { + opacity: 0.5; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/stylesV3.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/stylesV3.css new file mode 100644 index 0000000000..b61206b466 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/staleProp/stylesV3.css @@ -0,0 +1,3 @@ +p { + border-radius: 5px; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/asStatic.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/asStatic.css new file mode 100644 index 0000000000..ba894d0161 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/asStatic.css @@ -0,0 +1,3 @@ +p { + color: rgba(255, 0, 0, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/asStaticV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/asStaticV2.css new file mode 100644 index 0000000000..eeae35d47f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/asStaticV2.css @@ -0,0 +1,3 @@ +p { + color: rgba(0, 0, 255, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.css b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.css new file mode 100644 index 0000000000..1e375b2148 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.css @@ -0,0 +1,3 @@ +p { + font-style: italic; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.html b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.html new file mode 100644 index 0000000000..0cc51640e2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.js b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.js new file mode 100644 index 0000000000..896e075b59 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/shadow/usesStaticStylesheets/usesStaticStylesheets.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; +import asStatic from './asStatic.css'; +import asStaticV2 from './asStaticV2.css'; + +export default class extends LightningElement { + static stylesheets = [asStatic]; + + static asStatic = asStatic; + static asStaticV2 = asStaticV2; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/library/library.css b/packages/@lwc/integration-not-karma/test/swapping/styles/x/library/library.css new file mode 100644 index 0000000000..ae1ea2e00e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/library/library.css @@ -0,0 +1,3 @@ +p { + font-size: 10px; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.css b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.css new file mode 100644 index 0000000000..7eba4cd59a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.css @@ -0,0 +1,5 @@ +@import 'x/library'; + +p { + font-weight: 100; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.html b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.html new file mode 100644 index 0000000000..a095f9ef61 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.js b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.js new file mode 100644 index 0000000000..9280ff170c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/libraryUserA.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; +import style from './libraryUserA.css'; +import styleV2 from './styleV2.css'; + +export default class extends LightningElement { + static style = style; + static styleV2 = styleV2; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/styleV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/styleV2.css new file mode 100644 index 0000000000..96dbb56fc2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserA/styleV2.css @@ -0,0 +1,5 @@ +@import 'x/library'; + +p { + font-weight: 200; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.css b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.css new file mode 100644 index 0000000000..ce986121ba --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.css @@ -0,0 +1,5 @@ +@import 'x/library'; + +p { + font-weight: 800; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.html b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.html new file mode 100644 index 0000000000..a7d96b0cfe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.js b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.js new file mode 100644 index 0000000000..fdcb52eefd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/libraryUserB.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; +import style from './libraryUserB.css'; +import styleV2 from './styleV2.css'; + +export default class extends LightningElement { + static style = style; + static styleV2 = styleV2; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/styleV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/styleV2.css new file mode 100644 index 0000000000..052b8141e1 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryUserB/styleV2.css @@ -0,0 +1,5 @@ +@import 'x/library'; + +p { + font-weight: 900; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryV2/libraryV2.css b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryV2/libraryV2.css new file mode 100644 index 0000000000..20af690323 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/styles/x/libraryV2/libraryV2.css @@ -0,0 +1,3 @@ +p { + font-size: 20px; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/advanced/advanced.html b/packages/@lwc/integration-not-karma/test/swapping/templates/base/advanced/advanced.html new file mode 100644 index 0000000000..3e142e1ba4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/advanced/advanced.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/advanced/advanced.js b/packages/@lwc/integration-not-karma/test/swapping/templates/base/advanced/advanced.js new file mode 100644 index 0000000000..75caca0a4b --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/advanced/advanced.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; +import html from './advanced.html'; + +export default class Advanced extends LightningElement { + render() { + return html; + } + static baseTemplate = html; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/simple/simple.html b/packages/@lwc/integration-not-karma/test/swapping/templates/base/simple/simple.html new file mode 100644 index 0000000000..5aeaef9bbd --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/simple/simple.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/simple/simple.js b/packages/@lwc/integration-not-karma/test/swapping/templates/base/simple/simple.js new file mode 100644 index 0000000000..ead794fe91 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/simple/simple.js @@ -0,0 +1,6 @@ +import { LightningElement } from 'lwc'; +import html from './simple.html'; + +export default class Simple extends LightningElement { + static baseTemplate = html; +} diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/first.html b/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/first.html new file mode 100644 index 0000000000..9e5258593d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/first.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/second.html b/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/second.html new file mode 100644 index 0000000000..ca5d29f11a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/second.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/views.js b/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/views.js new file mode 100644 index 0000000000..da367db39a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/base/views/views.js @@ -0,0 +1,4 @@ +import first from './first.html'; +import second from './second.html'; + +export { first, second }; diff --git a/packages/@lwc/integration-not-karma/test/swapping/templates/index.spec.js b/packages/@lwc/integration-not-karma/test/swapping/templates/index.spec.js new file mode 100644 index 0000000000..dad5345046 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/swapping/templates/index.spec.js @@ -0,0 +1,48 @@ +import { createElement, swapTemplate } from 'lwc'; + +import Simple from 'base/simple'; +import Advanced from 'base/advanced'; +import { first, second } from 'base/views'; + +const simpleBaseTemplate = Simple.baseTemplate; +const advancedBaseTemplate = Advanced.baseTemplate; + +// Swapping is only enabled in dev mode +describe.skipIf(process.env.NODE_ENV === 'production')('template swapping', () => { + it('should work with components with implicit template definition', () => { + const elm = createElement('x-simple', { is: Simple }); + document.body.appendChild(elm); + expect(elm.shadowRoot.firstChild.outerHTML).toBe('

        simple

        '); + swapTemplate(simpleBaseTemplate, first); + return Promise.resolve() + .then(() => { + expect(elm.shadowRoot.firstChild.outerHTML).toBe('

        first

        '); + swapTemplate(first, second); + }) + .then(() => { + expect(elm.shadowRoot.firstChild.outerHTML).toBe('

        second

        '); + }); + }); + + it('should work with components with explict template definition', () => { + const elm = createElement('x-advanced', { is: Advanced }); + document.body.appendChild(elm); + expect(elm.shadowRoot.firstChild.outerHTML).toBe('

        advanced

        '); + swapTemplate(advancedBaseTemplate, second); + return Promise.resolve().then(() => { + expect(elm.shadowRoot.firstChild.outerHTML).toBe('

        second

        '); + }); + }); + + it('should throw for invalid old template', () => { + expect(() => { + swapTemplate(function () {}, second); + }).toThrow(); + }); + + it('should throw for invalid new template', () => { + expect(() => { + swapTemplate(second, function () {}); + }).toThrow(); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/index.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/index.spec.js new file mode 100644 index 0000000000..482ef5c4e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/index.spec.js @@ -0,0 +1,32 @@ +import { createElement } from 'lwc'; +import Source from 'x/source'; +import Target from 'x/target'; + +describe.skipIf(process.env.NATIVE_SHADOW)('activeElement', () => { + it('can call shadowRoot.activeElement on transported node with no lwc:dom=manual', async () => { + const source = createElement('x-source', { is: Source }); + const target = createElement('x-target', { is: Target }); + document.body.appendChild(source); + document.body.appendChild(target); + await Promise.resolve(); + + const button = source.shadowRoot.querySelector('button'); + + // append directly to the element, not to some
        inside of it as recommended + target.appendChild(button); + + // make active + button.focus(); + + let activeElement; + + expect(() => { + activeElement = target.shadowRoot.activeElement; + }).toLogErrorDev( + /NodeOwnedBy\(\) should never be called with a node that is not a child node of/ + ); + + // synthetic shadow gets this wrong when lwc:dom=manual is not used, so just assert that it exists + expect(activeElement).not.toBeNull(); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/source/source.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/source/source.html new file mode 100644 index 0000000000..e3e45791a6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/source/source.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/source/source.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/source/source.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/source/source.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/target/target.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/target/target.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/target/target.html @@ -0,0 +1,2 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/target/target.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/target/target.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/active-element/x/target/target.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/index.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/index.spec.js new file mode 100644 index 0000000000..6130d52b4e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/index.spec.js @@ -0,0 +1,202 @@ +import { createElement } from 'lwc'; +import Test from 'x/test'; + +describe('add handleEvent support', () => { + describe('basic', () => { + function test(elm) { + let invoked = false; + const listenerObject = { + handleEvent() { + invoked = true; + }, + }; + elm.addEventListener('foo', listenerObject); + elm.dispatchEvent(new CustomEvent('foo')); + expect(invoked).toBe(true); + } + + it('lwc host element', () => { + test(createElement('x-test', { is: Test })); + }); + + it('native element', () => { + test(document.createElement('div')); + }); + + it('lwc shadow root', () => { + test(createElement('x-test', { is: Test }).shadowRoot); + }); + + it('native shadow root', () => { + test(document.createElement('div').attachShadow({ mode: 'open' })); + }); + }); + + describe('listener object mutation', () => { + function test(elm) { + let value; + const listenerObject = {}; + + listenerObject.handleEvent = () => { + value = 'first'; + }; + elm.addEventListener('foo', listenerObject); + listenerObject.handleEvent = () => { + value = 'second'; + }; + + elm.dispatchEvent(new CustomEvent('foo')); + expect(value).toBe('second'); + } + + it('lwc host element', () => { + test(createElement('x-test', { is: Test })); + }); + + it('native element', () => { + test(document.createElement('div')); + }); + + it('lwc shadow root', () => { + test(createElement('x-test', { is: Test }).shadowRoot); + }); + + it('native shadow root', () => { + test(document.createElement('div').attachShadow({ mode: 'open' })); + }); + }); +}); + +describe('remove handleEvent support', () => { + describe('listener object identity', () => { + function test(elm) { + let invoked = false; + const listenerObject = {}; + + listenerObject.handleEvent = () => { + invoked = true; + }; + elm.addEventListener('foo', listenerObject); + + listenerObject.handleEvent = () => { + invoked = true; + }; + elm.removeEventListener('foo', listenerObject); + + elm.dispatchEvent(new CustomEvent('foo')); + expect(invoked).toBe(false); + } + + it('lwc host element', () => { + test(createElement('x-test', { is: Test })); + }); + + it('native element', () => { + test(document.createElement('div')); + }); + + it('lwc shadow root', () => { + test(createElement('x-test', { is: Test }).shadowRoot); + }); + + it('native shadow root', () => { + test(document.createElement('div').attachShadow({ mode: 'open' })); + }); + }); + + describe('listener identity', () => { + function test(elm) { + let invoked = false; + const handleEvent = function () { + invoked = true; + }; + elm.addEventListener('foo', { handleEvent }); + elm.removeEventListener('foo', { handleEvent }); + elm.dispatchEvent(new CustomEvent('foo')); + expect(invoked).toBe(true); + } + + it('lwc host element', () => { + test(createElement('x-test', { is: Test })); + }); + + it('native element', () => { + test(document.createElement('div')); + }); + + it('lwc shadow root', () => { + test(createElement('x-test', { is: Test }).shadowRoot); + }); + + it('native shadow root', () => { + test(document.createElement('div').attachShadow({ mode: 'open' })); + }); + }); +}); + +describe('dedupe behavior for add handleEvent', () => { + describe('listener object identity', () => { + function test(elm) { + let count = 0; + const listenerObject = {}; + + listenerObject.handleEvent = () => { + count += 1; + }; + elm.addEventListener('foo', listenerObject); + + listenerObject.handleEvent = () => { + count += 1; + }; + elm.addEventListener('foo', listenerObject); + + elm.dispatchEvent(new CustomEvent('foo')); + expect(count).toBe(1); + } + + it('lwc host element', () => { + test(createElement('x-test', { is: Test })); + }); + + it('native element', () => { + test(document.createElement('div')); + }); + + it('lwc shadow root', () => { + test(createElement('x-test', { is: Test }).shadowRoot); + }); + + it('native shadow root', () => { + test(document.createElement('div').attachShadow({ mode: 'open' })); + }); + }); + + describe('listener identity', () => { + function test(elm) { + let count = 0; + const handleEvent = () => { + count += 1; + }; + elm.addEventListener('foo', { handleEvent }); + elm.addEventListener('foo', { handleEvent }); + elm.dispatchEvent(new CustomEvent('foo')); + expect(count).toBe(2); + } + + it('lwc host element', () => { + test(createElement('x-test', { is: Test })); + }); + + it('native element', () => { + test(document.createElement('div')); + }); + + it('lwc shadow root', () => { + test(createElement('x-test', { is: Test }).shadowRoot); + }); + + it('native shadow root', () => { + test(document.createElement('div').attachShadow({ mode: 'open' })); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/x/test/test.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/x/test/test.html new file mode 100644 index 0000000000..cc340bc4c9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/x/test/test.html @@ -0,0 +1 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/x/test/test.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/x/test/test.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/add-event-listener/x/test/test.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/index.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/index.spec.js new file mode 100644 index 0000000000..d7ae52a839 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/index.spec.js @@ -0,0 +1,32 @@ +import { createElement, setFeatureFlagForTest } from 'lwc'; +import { IS_SYNTHETIC_SHADOW_LOADED, isSyntheticShadowRootInstance } from 'test-utils'; +import Component from 'x/component'; + +describe.runIf(IS_SYNTHETIC_SHADOW_LOADED && !process.env.FORCE_NATIVE_SHADOW_MODE_FOR_TEST)( + 'DISABLE_SYNTHETIC_SHADOW', + () => { + describe('flag disabled', () => { + it('renders synthetic shadow', () => { + const elm = createElement('x-component', { is: Component }); + document.body.appendChild(elm); + expect(isSyntheticShadowRootInstance(elm.shadowRoot)).toBe(true); + }); + }); + + describe('flag enabled', () => { + beforeEach(() => { + setFeatureFlagForTest('DISABLE_SYNTHETIC_SHADOW', true); + }); + + afterEach(() => { + setFeatureFlagForTest('DISABLE_SYNTHETIC_SHADOW', false); + }); + + it('renders native shadow', () => { + const elm = createElement('x-component', { is: Component }); + document.body.appendChild(elm); + expect(isSyntheticShadowRootInstance(elm.shadowRoot)).toBe(false); + }); + }); + } +); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/x/component/component.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/x/component/component.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js new file mode 100644 index 0000000000..a683920571 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js @@ -0,0 +1,60 @@ +import { createElement } from 'lwc'; + +import Unstyled from 'x/unstyled'; +import Styled from 'x/styled'; + +afterEach(() => { + window.__lwcResetGlobalStylesheets(); +}); + +describe('dom manual sharing nodes', () => { + it('has correct styles when sharing nodes from styled to unstyled component', () => { + const unstyled = createElement('x-unstyled', { is: Unstyled }); + const styled = createElement('x-styled', { is: Styled }); + document.body.appendChild(unstyled); + document.body.appendChild(styled); + + return new Promise((resolve) => requestAnimationFrame(() => resolve())) + .then(() => { + expect( + getComputedStyle(unstyled.shadowRoot.querySelector('.manual')).color + ).toEqual('rgb(0, 0, 0)'); + expect(getComputedStyle(styled.shadowRoot.querySelector('.manual')).color).toEqual( + 'rgb(255, 0, 0)' + ); + + styled.insertManualNode(unstyled.getManualNode()); + return new Promise((resolve) => requestAnimationFrame(() => resolve())); + }) + .then(() => { + expect(getComputedStyle(styled.shadowRoot.querySelector('.manual')).color).toEqual( + 'rgb(255, 0, 0)' + ); + }); + }); + + it('has correct styles when sharing nodes from unstyled to styled component', () => { + const unstyled = createElement('x-unstyled', { is: Unstyled }); + const styled = createElement('x-styled', { is: Styled }); + document.body.appendChild(unstyled); + document.body.appendChild(styled); + + return new Promise((resolve) => requestAnimationFrame(() => resolve())) + .then(() => { + expect( + getComputedStyle(unstyled.shadowRoot.querySelector('.manual')).color + ).toEqual('rgb(0, 0, 0)'); + expect(getComputedStyle(styled.shadowRoot.querySelector('.manual')).color).toEqual( + 'rgb(255, 0, 0)' + ); + + unstyled.insertManualNode(styled.getManualNode()); + return new Promise((resolve) => requestAnimationFrame(() => resolve())); + }) + .then(() => { + expect( + getComputedStyle(unstyled.shadowRoot.querySelector('.manual')).color + ).toEqual('rgb(0, 0, 0)'); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.css b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.css new file mode 100644 index 0000000000..63a609c238 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.css @@ -0,0 +1,3 @@ +.manual { + color: red; +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.html new file mode 100644 index 0000000000..375282739e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.js new file mode 100644 index 0000000000..14672a2f08 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/styled/styled.js @@ -0,0 +1,19 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + renderedCallback() { + this.template.querySelector('div').innerHTML = '
        manual
        '; + } + + @api + getManualNode() { + return this.template.querySelector('.manual'); + } + + @api + insertManualNode(node) { + const div = this.template.querySelector('div'); + div.innerHTML = ''; // clear + div.appendChild(node); + } +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/unstyled/unstyled.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/unstyled/unstyled.html new file mode 100644 index 0000000000..375282739e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/unstyled/unstyled.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/unstyled/unstyled.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/unstyled/unstyled.js new file mode 100644 index 0000000000..14672a2f08 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/dom-manual-sharing-nodes/x/unstyled/unstyled.js @@ -0,0 +1,19 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + renderedCallback() { + this.template.querySelector('div').innerHTML = '
        manual
        '; + } + + @api + getManualNode() { + return this.template.querySelector('.manual'); + } + + @api + insertManualNode(node) { + const div = this.template.querySelector('div'); + div.innerHTML = ''; // clear + div.appendChild(node); + } +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/element-api.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/element-api.spec.js new file mode 100644 index 0000000000..9d193c9590 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/element-api.spec.js @@ -0,0 +1,416 @@ +import { createElement } from 'lwc'; + +import Container from 'x/container'; +import ParentSpecialized from 'x/parentSpecialized'; + +/* +
        + +

        ctx first text

        +
        + +

        slot-container text

        + +

        with-slot text

        + +
        +

        slotted text

        +
        +
        +
        +
        +
        + +

        slot-container text

        + +

        with-slot text

        + +
        +

        slotted text

        +
        +
        +
        +
        +
        +
        +

        ctx last text

        +
        +
        + */ +describe.skipIf(process.env.NATIVE_SHADOW)('synthetic shadow with patch flags OFF', () => { + let lwcElementInsideShadow, + divManuallyApendedToShadow, + elementInShadow, + slottedComponent, + slottedNode, + elementOutsideLWC, + rootLwcElement, + cmpShadow; + beforeEach(() => { + spyOn(console, 'warn'); // ignore warning about manipulating node without lwc:dom="manual" + const elm = createElement('x-container', { is: Container }); + + elementOutsideLWC = document.createElement('div'); + elementOutsideLWC.appendChild(elm); + + document.body.appendChild(elementOutsideLWC); + + rootLwcElement = elm; + lwcElementInsideShadow = elm; + divManuallyApendedToShadow = elm.shadowRoot.querySelector('div.manual-ctx'); + cmpShadow = elm.shadowRoot.querySelector('x-slot-container').shadowRoot; + elementInShadow = rootLwcElement.shadowRoot.querySelector('div'); + slottedComponent = cmpShadow.querySelector('x-with-slot'); + slottedNode = cmpShadow.querySelector('.slotted'); + }); + + describe('Element.prototype API', () => { + it('should keep behavior for innerHTML', () => { + expect(elementOutsideLWC.innerHTML.length).toBe(455); + expect(rootLwcElement.innerHTML.length).toBe(0); + expect(lwcElementInsideShadow.innerHTML.length).toBe(0); + + expect(divManuallyApendedToShadow.innerHTML.length).toBe(176); //

        slot-container text

        with + + expect(cmpShadow.innerHTML.length).toBe(99); + + expect(slottedComponent.innerHTML.length).toBe(46); + expect(slottedNode.innerHTML.length).toBe(19); + }); + + it('should keep behavior for outerHTML', () => { + expect(elementOutsideLWC.outerHTML.length).toBe(466); + expect(rootLwcElement.outerHTML.length).toBe(27); + expect(lwcElementInsideShadow.outerHTML.length).toBe(27); + + expect(divManuallyApendedToShadow.outerHTML.length).toBe(206); //

        slot-container text

        wi .... + + expect(cmpShadow.outerHTML).toBe(undefined); + + expect(slottedComponent.outerHTML.length).toBe(73); + expect(slottedNode.outerHTML.length).toBe(46); + }); + + it('should keep behavior for children', () => { + expect(elementOutsideLWC.children.length).toBe(1); + expect(rootLwcElement.children.length).toBe(0); + expect(lwcElementInsideShadow.children.length).toBe(0); + + expect(divManuallyApendedToShadow.children.length).toBe(1); + + expect(cmpShadow.children.length).toBe(2); + + expect(slottedComponent.children.length).toBe(1); + expect(slottedNode.children.length).toBe(1); + }); + + it('should keep behavior for firstElementChild', () => { + expect(elementOutsideLWC.firstElementChild.tagName).toBe('X-CONTAINER'); + expect(rootLwcElement.firstElementChild).toBe(null); + expect(lwcElementInsideShadow.firstElementChild).toBe(null); + + expect(divManuallyApendedToShadow.firstElementChild.tagName).toBe( + 'X-MANUALLY-INSERTED' + ); + + expect(cmpShadow.firstElementChild.tagName).toBe('P'); + + expect(slottedComponent.firstElementChild.tagName).toBe('DIV'); + expect(slottedNode.firstElementChild.tagName).toBe('P'); + }); + + it('should keep behavior for lastElementChild', () => { + expect(elementOutsideLWC.lastElementChild.tagName).toBe('X-CONTAINER'); + expect(rootLwcElement.lastElementChild).toBe(null); + expect(lwcElementInsideShadow.lastElementChild).toBe(null); + + expect(divManuallyApendedToShadow.lastElementChild.tagName).toBe('X-MANUALLY-INSERTED'); + + expect(cmpShadow.lastElementChild.tagName).toBe('X-WITH-SLOT'); + + expect(slottedComponent.lastElementChild.tagName).toBe('DIV'); + expect(slottedNode.lastElementChild.tagName).toBe('P'); + }); + + describe('querySelector', () => { + it('should preserve element outside lwc boundary behavior', () => { + expect(elementOutsideLWC.querySelector('p').innerText).toBe('ctx first text'); + expect(elementOutsideLWC.querySelector('x-with-slot p').innerText).toBe( + 'with-slot text' + ); + expect(elementOutsideLWC.querySelector('.manual-ctx x-with-slot p').innerText).toBe( + 'with-slot text' + ); + expect(elementOutsideLWC.querySelector('div.slotted')).not.toBe(null); + }); + + it('should preserve root custom element behavior', () => { + expect(rootLwcElement.querySelector('p')).toBe(null); + expect(rootLwcElement.querySelector('x-with-slot p')).toBe(null); + expect(rootLwcElement.querySelector('.manual-ctx x-with-slot p')).toBe(null); + }); + + it('should preserve behavior for element inside shadow', () => { + const elemInShadow = rootLwcElement.shadowRoot.querySelector('div'); + + expect(elemInShadow.querySelector('x-slot-container')).not.toBe(null); + expect(elemInShadow.querySelector('x-with-slot p')).toBe(null); + }); + + it('should preserve behavior for shadowRoot', () => { + expect(cmpShadow.querySelector('p').innerText).toBe('slot-container text'); + expect(cmpShadow.querySelector('x-with-slot p').innerText).toBe('slotted text'); // skipped the one in the shadow of x-with-slot. + }); + + it('should preserve behavior for manually inserted element in shadow and with lwc components', () => { + expect(divManuallyApendedToShadow.querySelector('p').innerText).toBe( + 'slot-container text' + ); + expect(divManuallyApendedToShadow.querySelector('x-with-slot p').innerText).toBe( + 'with-slot text' + ); + expect(divManuallyApendedToShadow.querySelector('div.slotted')).not.toBe(null); + }); + }); + + it('should preserve behavior for querySelectorAll', () => { + expect(elementOutsideLWC.querySelectorAll('p').length).toBe(8); + expect(rootLwcElement.querySelectorAll('p').length).toBe(0); + + const elemInShadow = rootLwcElement.shadowRoot.querySelector('div'); + + // everything is inside a shadow, :+1: + expect(elemInShadow.querySelectorAll('p').length).toBe(0); + + expect(cmpShadow.querySelectorAll('p').length).toBe(2); // slotted elements + expect(slottedComponent.querySelectorAll('p').length).toBe(1); + expect(divManuallyApendedToShadow.querySelectorAll('p').length).toBe(3); + }); + + it('should preserve behavior for getElementsByTagName', () => { + expect(elementOutsideLWC.getElementsByTagName('p').length).toBe(8); + // This is an exception: not patching root lwc elements + expect(rootLwcElement.getElementsByTagName('p').length).toBe(8); + + // f, same, restricting this. + // const elemInShadow = rootLwcElement.shadowRoot.querySelector('div'); + // expect(elemInShadow.getElementsByTagName('p').length).toBe(6); + + // getElementsByTagName is not supported in the shadowRoot + // expect(cmpShadow.getElementsByTagName('p').length).toBe(2); + + // f: restricting, you should only get 1, that is inside the slot + // expect(slottedComponent.getElementsByTagName('p').length).toBe(2); + expect(slottedComponent.getElementsByTagName('p').length).toBe(1); + + expect(divManuallyApendedToShadow.getElementsByTagName('p').length).toBe(3); + }); + + it('should preserve behavior for getElementsByClassName', () => { + expect(elementOutsideLWC.getElementsByClassName('slotted').length).toBe(2); + // This is an exception: not patching root lwc elements + expect(rootLwcElement.getElementsByClassName('slotted').length).toBe(2); + + // f: inside shadow + // const elemInShadow = rootLwcElement.shadowRoot.querySelector('div'); + // expect(elemInShadow.getElementsByClassName('slotted').length).toBe(2); + + // getElementsByTagName is not supported in the shadowRoot + // expect(cmpShadow.getElementsByTagName('p').length).toBe(2); + + expect(slottedComponent.getElementsByClassName('slotted').length).toBe(1); + + expect(divManuallyApendedToShadow.getElementsByClassName('slotted').length).toBe(1); + }); + }); + + describe('Node.prototype API', () => { + it('should preserve behaviour for firstChild', () => { + expect(elementOutsideLWC.firstChild.tagName).toBe('X-CONTAINER'); + expect(rootLwcElement.firstChild).toBe(null); + expect(lwcElementInsideShadow.firstChild).toBe(null); + + expect(elementInShadow.firstChild.tagName).toBe('X-SLOT-CONTAINER'); + expect(divManuallyApendedToShadow.firstChild.tagName).toBe('X-MANUALLY-INSERTED'); + + expect(cmpShadow.firstChild.tagName).toBe('P'); + + expect(slottedComponent.firstChild.tagName).toBe('DIV'); + expect(slottedNode.firstChild.tagName).toBe('P'); + }); + + it('should preserve behaviour for lastChild', () => { + expect(elementOutsideLWC.lastChild.tagName).toBe('X-CONTAINER'); + expect(rootLwcElement.lastChild).toBe(null); + expect(lwcElementInsideShadow.lastChild).toBe(null); + + expect(elementInShadow.lastChild.tagName).toBe('DIV'); + expect(divManuallyApendedToShadow.lastChild.tagName).toBe('X-MANUALLY-INSERTED'); + + expect(cmpShadow.lastChild.tagName).toBe('X-WITH-SLOT'); + + expect(slottedComponent.lastChild.tagName).toBe('DIV'); + expect(slottedNode.lastChild.tagName).toBe('P'); + }); + + it('should preserve behaviour for textContent', () => { + expect(elementOutsideLWC.textContent.length).toBe(117); + expect(rootLwcElement.textContent.length).toBe(0); + expect(lwcElementInsideShadow.textContent.length).toBe(0); + + expect(elementInShadow.textContent.length).toBe(0); + expect(divManuallyApendedToShadow.textContent.length).toBe(45); + + expect(cmpShadow.textContent.length).toBe(31); + + expect(slottedComponent.textContent.length).toBe(12); + expect(slottedNode.textContent.length).toBe(12); + }); + + it('should preserve behaviour for parentNode', () => { + expect(elementOutsideLWC.parentNode.tagName).toBe('BODY'); + expect(rootLwcElement.parentNode.tagName).toBe('DIV'); + expect(lwcElementInsideShadow.parentNode.tagName).toBe('DIV'); + + expect(elementInShadow.parentNode).toBe(rootLwcElement.shadowRoot); + expect(divManuallyApendedToShadow.parentNode.tagName).toBe('DIV'); + + expect(cmpShadow.parentNode).toBe(null); + + const slotContainer = rootLwcElement.shadowRoot.querySelector('x-slot-container'); + expect(slottedComponent.parentNode).toBe(slotContainer.shadowRoot); + + // Note: check, but this is may be difference with the native shadow + expect(slottedNode.parentNode.tagName).toBe('X-WITH-SLOT'); + }); + + it('should preserve parentNode behavior when node was manually inserted', () => { + // this is a specialized test only for parentNode and parentElement + const lwcElem = createElement('x-parent-specialized', { is: ParentSpecialized }); + const containingElement = document.createElement('div'); + containingElement.appendChild(lwcElem); + document.body.appendChild(containingElement); + + const lwcRenderedNode = lwcElem.shadowRoot.querySelector('.lwc-rendered'); + const manualRenderedNode = lwcElem.shadowRoot.querySelector('.manual-rendered'); + + expect(lwcRenderedNode.parentNode).toBe(lwcElem.shadowRoot); + // is returning the custom element instead of the shadow root + expect(manualRenderedNode.parentNode).toBe(lwcElem); + }); + + it('should preserve behaviour for parentElement', () => { + expect(elementOutsideLWC.parentElement.tagName).toBe('BODY'); + expect(rootLwcElement.parentElement.tagName).toBe('DIV'); + expect(lwcElementInsideShadow.parentElement.tagName).toBe('DIV'); + + expect(elementInShadow.parentElement).toBe(null); + expect(divManuallyApendedToShadow.parentElement.tagName).toBe('DIV'); + + expect(cmpShadow.parentElement).toBe(null); + + expect(slottedComponent.parentElement).toBe(null); + + // Note: check, but this is may be difference with the native shadow + expect(slottedNode.parentElement.tagName).toBe('X-WITH-SLOT'); + }); + + it('should preserve parentElement behavior when node was manually inserted', () => { + // this is a specialized test only for parentNode and parentElement + const lwcElem = createElement('x-parent-specialized', { is: ParentSpecialized }); + const containingElement = document.createElement('div'); + containingElement.appendChild(lwcElem); + document.body.appendChild(containingElement); + + const lwcRenderedNode = lwcElem.shadowRoot.querySelector('.lwc-rendered'); + const manualRenderedNode = lwcElem.shadowRoot.querySelector('.manual-rendered'); + + expect(lwcRenderedNode.parentElement).toBe(null); + // is returning the custom element instead of the shadow root + expect(manualRenderedNode.parentElement).toBe(lwcElem); + }); + + it('should preserve childNodes behavior', () => { + expect(elementOutsideLWC.childNodes.length).toBe(1); + + expect(rootLwcElement.childNodes.length).toBe(0); + expect(lwcElementInsideShadow.childNodes.length).toBe(0); + expect(slottedComponent.childNodes.length).toBe(1); + + expect(divManuallyApendedToShadow.childNodes.length).toBe(1); + + expect(cmpShadow.childNodes.length).toBe(2); + + expect(slottedNode.childNodes.length).toBe(1); + }); + + it('should preserve hasChildNodes behavior', () => { + expect(elementOutsideLWC.hasChildNodes()).toBe(true); + expect(rootLwcElement.hasChildNodes()).toBe(false); + expect(lwcElementInsideShadow.hasChildNodes()).toBe(false); + + expect(divManuallyApendedToShadow.hasChildNodes()).toBe(true); + + expect(cmpShadow.hasChildNodes()).toBe(true); + + expect(slottedComponent.hasChildNodes()).toBe(true); + expect(slottedNode.hasChildNodes()).toBe(true); + }); + + it('should preserve compareDocumentPosition behavior', () => { + expect( + elementOutsideLWC.compareDocumentPosition(lwcElementInsideShadow) & + Node.DOCUMENT_POSITION_CONTAINED_BY + ).toBeGreaterThan(0); + + expect( + rootLwcElement.compareDocumentPosition(elementOutsideLWC) & + Node.DOCUMENT_POSITION_CONTAINS + ).toBeGreaterThan(0); + expect( + lwcElementInsideShadow.compareDocumentPosition(divManuallyApendedToShadow) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeGreaterThan(0); + + expect( + divManuallyApendedToShadow.compareDocumentPosition(elementOutsideLWC) & + Node.DOCUMENT_POSITION_CONTAINS + ).toBeGreaterThan(0); + + expect( + cmpShadow.compareDocumentPosition(slottedNode) & Node.DOCUMENT_POSITION_CONTAINED_BY + ).toBeGreaterThan(0); + }); + + it('should preserve contains behavior', () => { + expect(elementOutsideLWC.contains(lwcElementInsideShadow)).toBe(true); + + expect(rootLwcElement.contains(elementOutsideLWC)).toBe(false); + expect(lwcElementInsideShadow.contains(divManuallyApendedToShadow)).toBe(true); + + expect(divManuallyApendedToShadow.contains(elementOutsideLWC)).toBe(false); + + expect(cmpShadow.contains(slottedNode)).toBe(true); + }); + }); +}); + +describe('synthetic shadow for mixed mode', () => { + describe('Element.prototype API', () => { + it('should preseve assignedSlot behavior', () => { + const div = document.createElement('div'); + document.body.appendChild(div); + + div.attachShadow({ mode: 'open' }).innerHTML = ` + + `; + + const slotted = document.createElement('div'); + slotted.textContent = 'slotted'; + div.appendChild(slotted); + + const assignedSlot = div.shadowRoot.querySelector('slot'); + expect(slotted.assignedSlot).toBe(assignedSlot); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/container/container.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/container/container.html new file mode 100644 index 0000000000..c43f41c5de --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/container/container.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/container/container.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/container/container.js new file mode 100644 index 0000000000..a54aa8855e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/container/container.js @@ -0,0 +1,16 @@ +import { LightningElement, createElement } from 'lwc'; +import SlotContainer from 'x/slotContainer'; + +export default class Container extends LightningElement { + renderedCallback() { + const templateDiv = this.template.querySelector('div'); + const createdDiv = document.createElement('div'); + createdDiv.classList.add('manual-ctx'); + + const cmp = createElement('x-manually-inserted', { is: SlotContainer }); + createdDiv.appendChild(cmp); + + // this.template.insertBefore(createdDiv, lastParagraph); + templateDiv.insertBefore(createdDiv, undefined); + } +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/parentSpecialized/parentSpecialized.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/parentSpecialized/parentSpecialized.html new file mode 100644 index 0000000000..6c6b798087 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/parentSpecialized/parentSpecialized.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/parentSpecialized/parentSpecialized.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/parentSpecialized/parentSpecialized.js new file mode 100644 index 0000000000..3522d28539 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/parentSpecialized/parentSpecialized.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class ParentSpecialized extends LightningElement { + renderedCallback() { + const createdDiv = document.createElement('div'); + createdDiv.classList.add('manual-rendered'); + + this.template.appendChild(createdDiv); + } +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/slotContainer/slotContainer.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/slotContainer/slotContainer.html new file mode 100644 index 0000000000..1de5fd864a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/slotContainer/slotContainer.html @@ -0,0 +1,6 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/slotContainer/slotContainer.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/slotContainer/slotContainer.js new file mode 100644 index 0000000000..fd4a5c2e96 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/slotContainer/slotContainer.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class SlotContainer extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/withSlot/withSlot.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/withSlot/withSlot.html new file mode 100644 index 0000000000..a2f6d04671 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/withSlot/withSlot.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/withSlot/withSlot.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/withSlot/withSlot.js new file mode 100644 index 0000000000..88c310b151 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/x/withSlot/withSlot.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class WithSlot extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/index.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/index.spec.js new file mode 100644 index 0000000000..a695f4d476 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/index.spec.js @@ -0,0 +1,17 @@ +import { createElement } from 'lwc'; +import Component from 'x/component'; + +// TODO [#2922]: remove this test when we can support document.adoptedStyleSheets. +// Currently we can't, due to backwards compat. + +describe.skipIf(process.env.NATIVE_SHADOW)('global styles', () => { + it('injects global styles in document.head in synthetic shadow', () => { + const numStyleSheetsBefore = document.styleSheets.length; + const elm = createElement('x-component', { is: Component }); + document.body.appendChild(elm); + return Promise.resolve().then(() => { + const numStyleSheetsAfter = document.styleSheets.length; + expect(numStyleSheetsBefore + 1).toEqual(numStyleSheetsAfter); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.css b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.css new file mode 100644 index 0000000000..b658ab0251 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.css @@ -0,0 +1,4 @@ +h1 { + color: burlywood; + background: darkslategray; +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.html new file mode 100644 index 0000000000..85a44655fc --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/global-styles/x/component/component.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/index.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/index.spec.js new file mode 100644 index 0000000000..de92292ead --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/index.spec.js @@ -0,0 +1,37 @@ +import { createElement } from 'lwc'; +import Valid from 'x/valid'; +import Invalid from 'x/invalid'; + +describe('host pseudo', () => { + function testComponent(Component, name, test = it) { + test(`supports :host() pseudo class - ${name}`, async () => { + const elm = createElement('x-component', { is: Component }); + document.body.appendChild(elm); + const div = elm.shadowRoot.querySelector('.quux'); + + // expected styles for the div based on classes added to the shadow host + const expectedStyles = [ + ['', 'rgb(0, 0, 0)', '0px'], + ['foo', 'rgb(0, 128, 0)', '17px'], + ['bar', 'rgb(0, 128, 0)', '17px'], + ['foo bar', 'rgb(0, 128, 0)', '17px'], + ]; + + for (const [className, color, marginLeft] of expectedStyles) { + const oldClassName = elm.className; + elm.className += ' ' + className; + await new Promise((resolve) => requestAnimationFrame(resolve)); + expect(getComputedStyle(div).color).toEqual(color); + expect(getComputedStyle(div).marginLeft).toEqual(marginLeft); + elm.className = oldClassName; // reset so we keep the scope token + } + }); + } + + // TODO [#3225]: we should not support selector lists in :host() + testComponent(Invalid, 'invalid syntax', it.skipIf(process.env.NATIVE_SHADOW)); + + // Here we are using the correct syntax here for :host(), so it should work in both native and synthetic shadow + // See: https://github.com/salesforce/lwc/issues/3225 + testComponent(Valid, 'valid syntax'); +}); diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.css b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.css new file mode 100644 index 0000000000..fd531b84a8 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.css @@ -0,0 +1,4 @@ +/* this syntax is technically invalid https://github.com/salesforce/lwc/issues/3225 */ +:host(.foo, .bar) .baz .quux { + margin-left: 17px; +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.html new file mode 100644 index 0000000000..5dcd01ee7f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.js new file mode 100644 index 0000000000..dc6f09f6e5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.js @@ -0,0 +1,2 @@ +import { LightningElement } from 'lwc'; +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.scoped.css b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.scoped.css new file mode 100644 index 0000000000..498803dc8e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/invalid/invalid.scoped.css @@ -0,0 +1,4 @@ +/* this syntax is technically invalid https://github.com/salesforce/lwc/issues/3225 */ +:host(.foo, .bar) .baz .quux { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.css b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.css new file mode 100644 index 0000000000..336f9f945c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.css @@ -0,0 +1,4 @@ +:host(.foo) .baz .quux, +:host(.bar) .baz .quux { + margin-left: 17px; +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.html b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.html new file mode 100644 index 0000000000..5dcd01ee7f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.js new file mode 100644 index 0000000000..dc6f09f6e5 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.js @@ -0,0 +1,2 @@ +import { LightningElement } from 'lwc'; +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.scoped.css b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.scoped.css new file mode 100644 index 0000000000..648800b6fe --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/host-pseudo/x/valid/valid.scoped.css @@ -0,0 +1,4 @@ +:host(.foo) .baz .quux, +:host(.bar) .baz .quux { + color: green; +} diff --git a/packages/@lwc/integration-not-karma/test/synthetic-shadow/inner-outer-text/inner-outer-text.spec.js b/packages/@lwc/integration-not-karma/test/synthetic-shadow/inner-outer-text/inner-outer-text.spec.js new file mode 100644 index 0000000000..4b0854781c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/inner-outer-text/inner-outer-text.spec.js @@ -0,0 +1,174 @@ +import { createElement } from 'lwc'; +import Container from 'x/container'; + +// Note: originally these tests tested the runtime flag `ENABLE_INNER_OUTER_TEXT_PATCH`. +// After https://github.com/salesforce/lwc/pull/3103 though, this became tests for existing +// synthetic shadow behavior, which is not necessarily consistent with native shadow behavior. +// If you're wondering why so many of the tests are doing toMatch() on a regex, it's because of +// differences in how browsers serialize text using innerText/outerText. +describe.skipIf(process.env.NATIVE_SHADOW)('innerText and outerText', () => { + describe('innerText', () => { + let elm; + beforeEach(() => { + elm = createElement('x-container', { is: Container }); + document.body.appendChild(elm); + }); + + it('should return textContent when text within element is not selectable', () => { + const testCase = elm.shadowRoot.querySelector('.non-selectable-text'); + + expect(testCase.innerText).toBe('non selectable text'); + }); + + it('should remove consecutive LF in between from partial results', () => { + const testCase = elm.shadowRoot.querySelector('.consecutive-LF'); + + expect(testCase.innerText).toMatch( + /initial\s+first case text\s+second case text\s+end/ + ); + }); + + it('should remove hidden text + removes empty text between LF counts', () => { + const testCase = elm.shadowRoot.querySelector('.hidden-text-in-between'); + + expect(testCase.innerText).toBe(`initialend`); + }); + + it('should remove hidden text with css', () => { + const testCase = elm.shadowRoot.querySelector('.hidden-text-with-css'); + + expect(testCase.innerText).toBe(`initialend`); + }); + + it('should display textContent if element is hidden', () => { + const testCase = elm.shadowRoot.querySelector('.element-hidden-shows-text-content'); + + expect(testCase.innerText).toBe(`initialfirst case textsecond case textend`); + }); + + it('should return empty when childNodes are hidden', () => { + const testCase = elm.shadowRoot.querySelector('.empty-inner-text-due-hidden-children'); + + expect(testCase.innerText).toBe(``); + }); + + it('should collect text from multiple levels', () => { + const testCase = elm.shadowRoot.querySelector('.collect-text-multiple-levels'); + + expect(testCase.innerText).toMatch( + /This is, a text that should be displayed, in one line\. It includes links\.\s+Also paragraphs\s+and then another text/ + ); + }); + + it('should collect text from tables (table-cell and table-row)', () => { + const testCase = elm.shadowRoot.querySelector('.table-testcase'); + + // Notice that: + // 1. the last \t on each row is incorrect, it's a relaxation from the spec. + // 2. the last \n is incorrect, it's also a relaxation from the spec. + expect(testCase.innerText).toMatch(/1,1\s+1,2\s+2,1\s+2,2/); + }); + + it('should collect text from select', () => { + const testCase = elm.shadowRoot.querySelector('.select-testcase'); + + // Safari does not serialize innerText from