diff --git a/.github/workflows/karma.yml b/.github/workflows/karma.yml index 841704d518..51fc27b5e8 100644 --- a/.github/workflows/karma.yml +++ b/.github/workflows/karma.yml @@ -58,7 +58,9 @@ jobs: - run: LEGACY_BROWSERS=1 yarn sauce:ci - run: FORCE_NATIVE_SHADOW_MODE_FOR_TEST=1 yarn sauce:ci - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn sauce:ci + - run: DISABLE_DETACHED_REHYDRATION=1 yarn sauce:ci - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 DISABLE_SYNTHETIC=1 yarn sauce:ci + - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 DISABLE_DETACHED_REHYDRATION=1 yarn sauce:ci - name: Upload coverage results uses: actions/upload-artifact@v4 @@ -187,6 +189,8 @@ jobs: - run: NODE_ENV_FOR_TEST=production yarn hydration:sauce:ci:engine-server - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn hydration:sauce:ci:engine-server - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn hydration:sauce:ci:engine-server + - run: DISABLE_DETACHED_REHYDRATION=1 yarn hydration:sauce:ci:engine-server + - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 DISABLE_DETACHED_REHYDRATION=1 yarn hydration:sauce:ci:engine-server - run: yarn hydration:sauce:ci - run: ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION=1 yarn hydration:sauce:ci - run: NODE_ENV_FOR_TEST=production yarn hydration:sauce:ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..eac5cce729 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,106 @@ +name: Manual Release + +on: + workflow_dispatch: + inputs: + bump: + description: 'Version bump type (used if release_version is empty)' + type: choice + options: + - major + - minor + - patch + - prerelease + required: false + release_version: + description: 'Semver version to release (must be > current root version)' + required: false + +permissions: + contents: write + pull-requests: write + id-token: write + packages: write + +jobs: + release: + # Allow only on master, spring*, summer*, winter*; + if: ${{ github.repository_owner == 'salesforce' && (github.ref_name == 'master' || startsWith(github.ref_name, 'spring') || startsWith(github.ref_name, 'summer') || startsWith(github.ref_name, 'winter')) }} + environment: release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.WORKFLOW_PAT }} + persist-credentials: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Resolve version input + id: resolve_version + run: | + RELEASE_VERSION='${{ inputs.release_version }}' + if [ -z "$RELEASE_VERSION" ]; then + RELEASE_VERSION='${{ inputs.bump }}' + fi + echo "resolved=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" + + - name: Resolve version + env: + INPUT_VERSION: ${{ steps.resolve_version.outputs.resolved }} + run: | + node ./scripts/release/version.js "$INPUT_VERSION" + RESOLVED_VERSION=$(jq -r .version package.json) + echo "RESOLVED_VERSION=$RESOLVED_VERSION" >> "$GITHUB_ENV" + + - name: Set git identity (Actions bot) + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Commit & push + uses: actions-js/push@v1.4 + with: + github_token: ${{ secrets.WORKFLOW_PAT }} + branch: ${{ github.ref_name }} + message: 'chore: release v${{ env.RESOLVED_VERSION }}' + + - name: Build + run: yarn build + + - name: Tag and create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=$(jq -r .version package.json) + git tag -a "v$VERSION" -m "Release v$VERSION" + git push origin tag "v$VERSION" + gh release create "v$VERSION" --title "v$VERSION" --generate-notes + + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_ALWAYS_AUTH: 'true' + run: | + # Force both npm and yarn to use npmjs and pick up the token + yarn config set registry https://registry.npmjs.org + npm config set registry https://registry.npmjs.org + printf "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\nalways-auth=true\n" > ~/.npmrc + + # Sanity checks + echo "yarn registry: $(yarn config get registry)" + echo "npm registry: $(npm config get registry)" + + TAG=$([ "$GITHUB_REF_NAME" = "master" ] && echo latest || echo "$GITHUB_REF_NAME") + yarn nx release publish --yes --tag "$TAG" diff --git a/.github/workflows/web-test-runner.yml b/.github/workflows/web-test-runner.yml index cda4e7333f..12199e8506 100644 --- a/.github/workflows/web-test-runner.yml +++ b/.github/workflows/web-test-runner.yml @@ -4,9 +4,11 @@ on: push: branches: - master + - release + - 'spring*' + - 'summer*' + - 'winter*' pull_request: - branches: - - master env: SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} @@ -20,7 +22,7 @@ jobs: # TODO: add env var combos we use for Karma tests # TODO: upload result artifacts # TODO: make it saucy 🥫 - run-wtr-tests-group-1: + wtr-group-1: runs-on: ubuntu-22.04 env: SAUCE_TUNNEL_ID: github-action-tunnel-wtr-${{github.run_id}}-group-1 @@ -50,11 +52,3 @@ jobs: - run: yarn test - run: yarn test:hydration - - run-karma-tests: - runs-on: ubuntu-22.04 - defaults: - run: - working-directory: ./packages/@lwc/integration-not-karma - needs: - - run-wtr-tests-group-1 diff --git a/.nucleus.yaml b/.nucleus.yaml index 4912d2a25c..78ce8e46b9 100644 --- a/.nucleus.yaml +++ b/.nucleus.yaml @@ -5,6 +5,7 @@ core-deploy: branches: ~DEFAULT~: pull-request: &branch-definition + workflow: build-and-test auto-start: true auto-start-from-forks: false merge-method: disabled # do not auto-merge; we'll do it ourselves @@ -31,7 +32,6 @@ branches: release: pull-request: <<: *branch-definition - merge-method: force-push # release branch should always be in sync with master branch (linear history) # Only active branches need to be included in this config winter26: pull-request: @@ -62,16 +62,3 @@ steps: # this project runs yarn build after yarn install so skip explicit build step node-build: &node-build skip: true - node-pre-release-tests: - params: - command: yarn test - npm-configure: - params: - registry-url: https://registry.yarnpkg.com - npm-configure-for-publish: - params: - registry-url: https://registry.npmjs.org - npm-publish-release: - params: - access: public - tag: latest # note: this should be summer22, winter23, etc. if this .nucleus.yaml file is in a non-master branch diff --git a/eslint.config.mjs b/eslint.config.mjs index f890cfb35a..494c669bdc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -355,7 +355,18 @@ export default tseslint.config( }, }, { - files: ['packages/@lwc/integration-karma/**', 'packages/@lwc/integration-not-karma/**'], + files: ['packages/@lwc/integration-not-karma/**'], + + languageOptions: { + globals: { + lwcRuntimeFlags: true, + process: true, + ...globals.browser, + }, + }, + }, + { + files: ['packages/@lwc/integration-karma/**'], languageOptions: { globals: { @@ -367,11 +378,6 @@ export default tseslint.config( ...globals.jasmine, }, }, - - rules: { - 'no-var': 'off', - 'prefer-rest-params': 'off', - }, }, { files: ['packages/@lwc/synthetic-shadow/**'], diff --git a/package.json b/package.json index 53f2aad12e..f7bad8970e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lwc-monorepo", - "version": "8.21.2", + "version": "8.22.2", "private": true, "description": "Lightning Web Components", "repository": { @@ -38,43 +38,43 @@ }, "devDependencies": { "@commitlint/cli": "^19.8.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.35.0", "@lwc/eslint-plugin-lwc-internal": "link:./scripts/eslint-plugin", "@lwc/test-utils-lwc-internals": "link:./scripts/test-utils", - "@nx/js": "21.3.11", + "@nx/js": "21.4.1", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-typescript": "^12.1.4", - "@swc-node/register": "~1.10.10", - "@swc/core": "~1.13.3", + "@swc-node/register": "~1.11.1", + "@swc/core": "~1.13.5", "@swc/helpers": "~0.5.17", "@types/babel__core": "^7.20.5", - "@types/node": "^22.17.1", + "@types/node": "^22.18.1", "@vitest/coverage-v8": "^3.2.4", - "@vitest/eslint-plugin": "^1.3.4", + "@vitest/eslint-plugin": "^1.3.9", "@vitest/ui": "^3.2.4", "bytes": "^3.1.2", "es-module-lexer": "^1.7.0", - "eslint": "9.33.0", + "eslint": "9.35.0", "eslint-config-flat-gitignore": "^2.1.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.32.0", "glob": "^11.0.3", "globals": "^16.3.0", "husky": "^9.1.7", - "isbinaryfile": "^5.0.4", + "isbinaryfile": "^5.0.6", "jsdom": "^26.1.0", - "lint-staged": "^16.1.5", - "magic-string": "^0.30.17", - "nx": "21.3.11", + "lint-staged": "^16.1.6", + "magic-string": "^0.30.19", + "nx": "21.4.1", "prettier": "^3.6.2", - "rollup": "^4.46.2", - "terser": "^5.43.1", + "rollup": "^4.50.1", + "terser": "^5.44.0", "tslib": "^2.8.1", "typescript": "5.8.2", - "typescript-eslint": "8.39.0", + "typescript-eslint": "8.42.0", "vitest": "^3.2.4" }, "lint-staged": { @@ -98,14 +98,12 @@ "resolutions": { "//": { "http-cache-semantics": "Pinned to address security vulnerability", - "semver": "Pinned to address security vulnerability", "@types/estree": [ "Used by us and our dependencies. Because it's a type definition package,", "we need everyone to use the same types (mixing versions breaks stuff)." ] }, - "http-cache-semantics": "4.1.1", - "semver": "7.6.0", + "http-cache-semantics": "4.2.0", "@types/estree": "^1.0.8" }, "dependencies": {} diff --git a/packages/@lwc/aria-reflection/package.json b/packages/@lwc/aria-reflection/package.json index 07a146a46e..1aa62bddce 100644 --- a/packages/@lwc/aria-reflection/package.json +++ b/packages/@lwc/aria-reflection/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/aria-reflection", - "version": "8.21.2", + "version": "8.22.2", "description": "ARIA element reflection polyfill for strings", "keywords": [ "aom", diff --git a/packages/@lwc/babel-plugin-component/package.json b/packages/@lwc/babel-plugin-component/package.json index 5cb1599edd..beba283ac5 100644 --- a/packages/@lwc/babel-plugin-component/package.json +++ b/packages/@lwc/babel-plugin-component/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/babel-plugin-component", - "version": "8.21.2", + "version": "8.22.2", "description": "Babel plugin to transform a LWC module", "keywords": [ "lwc" @@ -47,8 +47,8 @@ }, "dependencies": { "@babel/helper-module-imports": "7.27.1", - "@lwc/errors": "8.21.2", - "@lwc/shared": "8.21.2", + "@lwc/errors": "8.22.2", + "@lwc/shared": "8.22.2", "line-column": "~1.0.2" }, "devDependencies": { diff --git a/packages/@lwc/compiler/package.json b/packages/@lwc/compiler/package.json index b597b6ebe1..a5af222a3f 100644 --- a/packages/@lwc/compiler/package.json +++ b/packages/@lwc/compiler/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/compiler", - "version": "8.21.2", + "version": "8.22.2", "description": "LWC compiler", "keywords": [ "lwc" @@ -46,17 +46,17 @@ } }, "dependencies": { - "@babel/core": "7.28.0", + "@babel/core": "7.28.4", "@babel/plugin-transform-async-generator-functions": "7.28.0", "@babel/plugin-transform-async-to-generator": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", - "@babel/plugin-transform-object-rest-spread": "7.28.0", + "@babel/plugin-transform-object-rest-spread": "7.28.4", "@locker/babel-plugin-transform-unforgeables": "0.22.0", - "@lwc/babel-plugin-component": "8.21.2", - "@lwc/errors": "8.21.2", - "@lwc/shared": "8.21.2", - "@lwc/ssr-compiler": "8.21.2", - "@lwc/style-compiler": "8.21.2", - "@lwc/template-compiler": "8.21.2" + "@lwc/babel-plugin-component": "8.22.2", + "@lwc/errors": "8.22.2", + "@lwc/shared": "8.22.2", + "@lwc/ssr-compiler": "8.22.2", + "@lwc/style-compiler": "8.22.2", + "@lwc/template-compiler": "8.22.2" } } diff --git a/packages/@lwc/engine-core/package.json b/packages/@lwc/engine-core/package.json index f22221c6db..27d244f81a 100644 --- a/packages/@lwc/engine-core/package.json +++ b/packages/@lwc/engine-core/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/engine-core", - "version": "8.21.2", + "version": "8.22.2", "description": "Core LWC engine APIs.", "keywords": [ "lwc" @@ -46,9 +46,9 @@ } }, "dependencies": { - "@lwc/features": "8.21.2", - "@lwc/shared": "8.21.2", - "@lwc/signals": "8.21.2" + "@lwc/features": "8.22.2", + "@lwc/shared": "8.22.2", + "@lwc/signals": "8.22.2" }, "devDependencies": { "observable-membrane": "2.0.0" diff --git a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts index 3ed830fc64..d28bd0225e 100644 --- a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts +++ b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { isNull, isObject, isTrustedSignal } from '@lwc/shared'; +import { isNull, isObject, isTrustedSignal, legacyIsTrustedSignal } from '@lwc/shared'; import { ReactiveObserver, valueMutated, valueObserved } from '../libs/mutation-tracker'; import { subscribeToSignal } from '../libs/signal-tracker'; import type { Signal } from '@lwc/signals'; @@ -41,13 +41,26 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = { lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS && isObject(target) && !isNull(target) && - isTrustedSignal(target) && process.env.IS_BROWSER && // Only subscribe if a template is being rendered by the engine tro.isObserving() ) { - // Subscribe the template reactive observer's notify method, which will mark the vm as dirty and schedule hydration. - subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); + /** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedSignals set. + * However, this resulted in a bug as all object values were + * being considered signals in environments where the trustedSignals + * set had not been defined. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ + if ( + lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION + ? legacyIsTrustedSignal(target) + : isTrustedSignal(target) + ) { + // Subscribe the template reactive observer's notify method, which will mark the vm as dirty and schedule hydration. + subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); + } } } diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 54db244bc1..e5c977d9b6 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -673,7 +673,14 @@ function flushRehydrationQueue() { for (let i = 0, len = vms.length; i < len; i += 1) { const vm = vms[i]; try { - rehydrate(vm); + // We want to prevent rehydration from occurring when nodes are detached from the DOM as this can trigger + // unintended side effects, like lifecycle methods being called multiple times. + // For backwards compatibility, we use a flag to control the check. + // 1. When flag is off, always rehydrate (legacy behavior) + // 2. When flag is on, only rehydrate when the VM state is connected (fixed behavior) + if (!lwcRuntimeFlags.DISABLE_DETACHED_REHYDRATION || vm.state === VMState.connected) { + rehydrate(vm); + } } catch (error) { if (i + 1 < len) { // pieces of the queue are still pending to be rehydrated, those should have priority diff --git a/packages/@lwc/engine-dom/package.json b/packages/@lwc/engine-dom/package.json index 719716f417..c3617fb98a 100644 --- a/packages/@lwc/engine-dom/package.json +++ b/packages/@lwc/engine-dom/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/engine-dom", - "version": "8.21.2", + "version": "8.22.2", "description": "Renders LWC components in a DOM environment.", "keywords": [ "lwc" @@ -46,10 +46,10 @@ } }, "devDependencies": { - "@lwc/engine-core": "8.21.2", - "@lwc/shared": "8.21.2", - "@lwc/features": "8.21.2", - "@lwc/signals": "8.21.2" + "@lwc/engine-core": "8.22.2", + "@lwc/shared": "8.22.2", + "@lwc/features": "8.22.2", + "@lwc/signals": "8.22.2" }, "lwc": { "modules": [ diff --git a/packages/@lwc/engine-server/package.json b/packages/@lwc/engine-server/package.json index 06a1d0585d..1f6b81af2e 100644 --- a/packages/@lwc/engine-server/package.json +++ b/packages/@lwc/engine-server/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/engine-server", - "version": "8.21.2", + "version": "8.22.2", "description": "Renders LWC components in a server environment.", "keywords": [ "lwc" @@ -46,11 +46,11 @@ } }, "devDependencies": { - "@lwc/engine-core": "8.21.2", - "@lwc/rollup-plugin": "8.21.2", - "@lwc/shared": "8.21.2", - "@lwc/features": "8.21.2", - "@lwc/signals": "8.21.2", + "@lwc/engine-core": "8.22.2", + "@lwc/rollup-plugin": "8.22.2", + "@lwc/shared": "8.22.2", + "@lwc/features": "8.22.2", + "@lwc/signals": "8.22.2", "@rollup/plugin-virtual": "^3.0.2", "parse5": "^8.0.0" } diff --git a/packages/@lwc/errors/package.json b/packages/@lwc/errors/package.json index 76f4d4622d..247300328a 100644 --- a/packages/@lwc/errors/package.json +++ b/packages/@lwc/errors/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/errors", - "version": "8.21.2", + "version": "8.22.2", "description": "LWC Error Utilities", "keywords": [ "lwc" diff --git a/packages/@lwc/errors/src/compiler/error-info/index.ts b/packages/@lwc/errors/src/compiler/error-info/index.ts index 55f70e8f28..4129bedf60 100644 --- a/packages/@lwc/errors/src/compiler/error-info/index.ts +++ b/packages/@lwc/errors/src/compiler/error-info/index.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ /** - * Next error code: 1207 + * Next error code: 1209 */ export * from './compiler'; diff --git a/packages/@lwc/errors/src/compiler/error-info/template-transform.ts b/packages/@lwc/errors/src/compiler/error-info/template-transform.ts index 3a86c7f486..70fa8f14b0 100644 --- a/packages/@lwc/errors/src/compiler/error-info/template-transform.ts +++ b/packages/@lwc/errors/src/compiler/error-info/template-transform.ts @@ -979,4 +979,20 @@ export const ParserDiagnostics = { level: DiagnosticLevel.Error, url: '', }, + + COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_COMPLEX: { + code: 1207, + message: + 'Template expression doesn\'t allow computed property access unless the expression is surrounded by quotes: "{0}"', + level: DiagnosticLevel.Error, + url: '', + }, + + INVALID_NODE_COMPLEX: { + code: 1208, + message: + 'Template expression doesn\'t allow {0} unless the expression is surrounded by quotes: "{1}"', + level: DiagnosticLevel.Error, + url: '', + }, }; diff --git a/packages/@lwc/features/package.json b/packages/@lwc/features/package.json index e34fc76531..94e6e8b563 100644 --- a/packages/@lwc/features/package.json +++ b/packages/@lwc/features/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/features", - "version": "8.21.2", + "version": "8.22.2", "description": "LWC Features Flags", "keywords": [ "lwc" @@ -46,6 +46,6 @@ } }, "dependencies": { - "@lwc/shared": "8.21.2" + "@lwc/shared": "8.22.2" } } diff --git a/packages/@lwc/features/src/index.ts b/packages/@lwc/features/src/index.ts index 2c4608d7b0..a60f471c9d 100644 --- a/packages/@lwc/features/src/index.ts +++ b/packages/@lwc/features/src/index.ts @@ -19,10 +19,12 @@ const features: FeatureFlagMap = { ENABLE_LEGACY_SCOPE_TOKENS: null, ENABLE_FORCE_SHADOW_MIGRATE_MODE: null, ENABLE_EXPERIMENTAL_SIGNALS: null, + ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION: null, DISABLE_SYNTHETIC_SHADOW: null, DISABLE_SCOPE_TOKEN_VALIDATION: null, LEGACY_LOCKER_ENABLED: null, DISABLE_LEGACY_VALIDATION: null, + DISABLE_DETACHED_REHYDRATION: null, }; if (!(globalThis as any).lwcRuntimeFlags) { diff --git a/packages/@lwc/features/src/types.ts b/packages/@lwc/features/src/types.ts index ab7984b7a8..c15f2faf28 100644 --- a/packages/@lwc/features/src/types.ts +++ b/packages/@lwc/features/src/types.ts @@ -70,6 +70,13 @@ export interface FeatureFlagMap { */ ENABLE_EXPERIMENTAL_SIGNALS: FeatureFlagValue; + /** + * If true, legacy signal validation is used, where all component properties are considered signals or context + * if a trustedSignalSet and trustedContextSet have not been provided via setTrustedSignalSet and setTrustedContextSet. + * This is a killswitch for a bug fix: #5492 + */ + ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION: FeatureFlagValue; + /** * If true, ignore `@lwc/synthetic-shadow` even if it's loaded on the page. Instead, run all components in * native shadow mode. @@ -93,6 +100,12 @@ export interface FeatureFlagMap { * If false or unset, then the value of the `LEGACY_LOCKER_ENABLED` flag is used. */ DISABLE_LEGACY_VALIDATION: FeatureFlagValue; + + /** + * If true, skips rehydration of DOM elements that are not connected. + * Applies to rehydration performed while flushing the rehydration queue. + */ + DISABLE_DETACHED_REHYDRATION: FeatureFlagValue; } export type FeatureFlagName = keyof FeatureFlagMap; diff --git a/packages/@lwc/integration-karma/package.json b/packages/@lwc/integration-karma/package.json index 9a701197e0..06f0c4afb4 100644 --- a/packages/@lwc/integration-karma/package.json +++ b/packages/@lwc/integration-karma/package.json @@ -1,7 +1,7 @@ { "name": "@lwc/integration-karma", "private": true, - "version": "8.21.2", + "version": "8.22.2", "scripts": { "start": "KARMA_MODE=watch karma start ./scripts/karma-configs/test/local.js", "test": "karma start ./scripts/karma-configs/test/local.js --single-run", @@ -21,16 +21,16 @@ "karma-sauce-launcher-fix-firefox": "using a fork to work around https://github.com/karma-runner/karma-sauce-launcher/issues/275" }, "devDependencies": { - "@lwc/compiler": "8.21.2", - "@lwc/engine-dom": "8.21.2", - "@lwc/engine-server": "8.21.2", - "@lwc/rollup-plugin": "8.21.2", - "@lwc/synthetic-shadow": "8.21.2", - "@types/jasmine": "^5.1.8", + "@lwc/compiler": "8.22.2", + "@lwc/engine-dom": "8.22.2", + "@lwc/engine-server": "8.22.2", + "@lwc/rollup-plugin": "8.22.2", + "@lwc/synthetic-shadow": "8.22.2", + "@types/jasmine": "^5.1.9", "chokidar": "^4.0.3", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.7", + "istanbul-reports": "^3.2.0", "karma": "6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-coverage": "^2.2.1", diff --git a/packages/@lwc/integration-karma/test-hydration/context/index.spec.js b/packages/@lwc/integration-karma/test-hydration/context/index.spec.js index ed71537fac..4e8ffea3c7 100644 --- a/packages/@lwc/integration-karma/test-hydration/context/index.spec.js +++ b/packages/@lwc/integration-karma/test-hydration/context/index.spec.js @@ -1,12 +1,4 @@ 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'); diff --git a/packages/@lwc/integration-karma/test/api/sanitizeAttribute/index.spec.js b/packages/@lwc/integration-karma/test/api/sanitizeAttribute/index.spec.js index 239ad4cae9..6d1d43f76a 100644 --- a/packages/@lwc/integration-karma/test/api/sanitizeAttribute/index.spec.js +++ b/packages/@lwc/integration-karma/test/api/sanitizeAttribute/index.spec.js @@ -36,11 +36,10 @@ const scenarios = [ scenarios.forEach(({ type, attrName, tagName, Ctor }) => { describe(`${type} ${attrName}`, () => { - const originalSanitizeAttribute = LWC.sanitizeAttribute; - + // Spy is created in a mock file and injected with the import map plugin + const sanitizeAttributeSpy = LWC.sanitizeAttribute; afterEach(() => { - // Reset original sanitizer after each test. - LWC.sanitizeAttribute = originalSanitizeAttribute; + sanitizeAttributeSpy.mockReset(); }); it('uses the original passthrough sanitizer when not overridden', () => { @@ -52,8 +51,6 @@ scenarios.forEach(({ type, attrName, tagName, Ctor }) => { }); it('receives the right parameters', () => { - spyOn(LWC, 'sanitizeAttribute'); - const elm = createElement(tagName, { is: Ctor }); document.body.appendChild(elm); @@ -66,7 +63,7 @@ scenarios.forEach(({ type, attrName, tagName, Ctor }) => { }); it('replace the original attribute value with a string', () => { - spyOn(LWC, 'sanitizeAttribute').and.returnValue('/bar'); + sanitizeAttributeSpy.mockReturnValue('/bar'); const elm = createElement(tagName, { is: Ctor }); document.body.appendChild(elm); @@ -76,7 +73,7 @@ scenarios.forEach(({ type, attrName, tagName, Ctor }) => { }); it('replace the original attribute value with undefined', () => { - spyOn(LWC, 'sanitizeAttribute').and.returnValue(undefined); + sanitizeAttributeSpy.mockReturnValue(undefined); const elm = createElement(tagName, { is: Ctor }); document.body.appendChild(elm); @@ -105,8 +102,6 @@ booleanTrueScenarios.forEach(({ attrName, tagName, Ctor }) => { describe(attrName, () => { // For boolean literals (e.g. ``), there is no reason to sanitize since it's empty it('does not sanitize when used as a boolean-true attribute', () => { - spyOn(LWC, 'sanitizeAttribute'); - const elm = createElement(tagName, { is: Ctor }); document.body.appendChild(elm); diff --git a/packages/@lwc/integration-karma/test/mixed-shadow-mode/synthetic-behavior/index.spec.js b/packages/@lwc/integration-karma/test/mixed-shadow-mode/synthetic-behavior/index.spec.js index cd1a0b7c3e..2a57066843 100644 --- a/packages/@lwc/integration-karma/test/mixed-shadow-mode/synthetic-behavior/index.spec.js +++ b/packages/@lwc/integration-karma/test/mixed-shadow-mode/synthetic-behavior/index.spec.js @@ -15,6 +15,10 @@ import GrandparentResetParentAnyChildReset from 'x/grandparentResetParentAnyChil import GrandparentResetParentResetChildAny from 'x/grandparentResetParentResetChildAny'; import GrandparentResetParentResetChildReset from 'x/grandparentResetParentResetChildReset'; +afterEach(() => { + window.__lwcResetGlobalStylesheets(); +}); + describe.skipIf(process.env.NATIVE_SHADOW)('synthetic behavior', () => { const scenarios = [ { diff --git a/packages/@lwc/integration-karma/test/rendering/style-specificity-important/index.spec.js b/packages/@lwc/integration-karma/test/rendering/style-specificity-important/index.spec.js index f7ebaaba16..e5f0d40d7f 100644 --- a/packages/@lwc/integration-karma/test/rendering/style-specificity-important/index.spec.js +++ b/packages/@lwc/integration-karma/test/rendering/style-specificity-important/index.spec.js @@ -1,6 +1,10 @@ import { createElement } from 'lwc'; import Component from 'x/component'; +afterEach(() => { + window.__lwcResetGlobalStylesheets(); +}); + describe('important styling and style override', () => { it('should render !important styles correctly', async () => { const elm = createElement('x-component', { is: Component }); diff --git a/packages/@lwc/integration-karma/test/shadow-dom/stylesheet/index.spec.js b/packages/@lwc/integration-karma/test/shadow-dom/stylesheet/index.spec.js index 2945904bef..ec6ee2909d 100644 --- a/packages/@lwc/integration-karma/test/shadow-dom/stylesheet/index.spec.js +++ b/packages/@lwc/integration-karma/test/shadow-dom/stylesheet/index.spec.js @@ -4,6 +4,10 @@ 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 }); diff --git a/packages/@lwc/integration-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js b/packages/@lwc/integration-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js index bff0ab7318..a683920571 100644 --- a/packages/@lwc/integration-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js +++ b/packages/@lwc/integration-karma/test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js @@ -3,6 +3,10 @@ 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 }); diff --git a/packages/@lwc/integration-karma/test/template/directive-lwc-dom-manual/index.spec.js b/packages/@lwc/integration-karma/test/template/directive-lwc-dom-manual/index.spec.js index 5f12e9b3cd..d2ee0da2ba 100644 --- a/packages/@lwc/integration-karma/test/template/directive-lwc-dom-manual/index.spec.js +++ b/packages/@lwc/integration-karma/test/template/directive-lwc-dom-manual/index.spec.js @@ -8,6 +8,10 @@ function waitForStyleToBeApplied() { return Promise.resolve(); } +afterEach(() => { + window.__lwcResetGlobalStylesheets(); +}); + describe('dom mutation without the lwc:dom="manual" directive', () => { function testErrorOnDomMutation(method, fn) { it(`should log a warning when calling ${method} on an element without the lwc:dom="manual" directive only in synthetic mode`, () => { diff --git a/packages/@lwc/integration-karma/test/template/directive-lwc-inner-html/index.spec.js b/packages/@lwc/integration-karma/test/template/directive-lwc-inner-html/index.spec.js index 5778caa3e9..5b3f8976b1 100644 --- a/packages/@lwc/integration-karma/test/template/directive-lwc-inner-html/index.spec.js +++ b/packages/@lwc/integration-karma/test/template/directive-lwc-inner-html/index.spec.js @@ -7,18 +7,18 @@ let originalSanitizeHtmlContent; beforeAll(() => { originalSanitizeHtmlContent = getHooks().sanitizeHtmlContent; - setHooks({ - sanitizeHtmlContent: (content) => content, - }); + setHooks({ sanitizeHtmlContent: (content) => content }); }); afterAll(() => { - setHooks({ - sanitizeHtmlContent: originalSanitizeHtmlContent, - }); + setHooks({ sanitizeHtmlContent: originalSanitizeHtmlContent }); +}); + +afterEach(() => { + window.__lwcResetGlobalStylesheets(); }); -it('renders the content as HTML', () => { +it('renders the content as HTML', async () => { const elm = createElement('x-inner-html', { is: XInnerHtml }); elm.content = 'Hello World'; document.body.appendChild(elm); diff --git a/packages/@lwc/integration-not-karma/configs/base.mjs b/packages/@lwc/integration-not-karma/configs/base.js similarity index 86% rename from packages/@lwc/integration-not-karma/configs/base.mjs rename to packages/@lwc/integration-not-karma/configs/base.js index 6e67acbd4a..41c8bde90f 100644 --- a/packages/@lwc/integration-not-karma/configs/base.mjs +++ b/packages/@lwc/integration-not-karma/configs/base.js @@ -1,6 +1,6 @@ import { join } from 'node:path'; import { LWC_VERSION } from '@lwc/shared'; -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}');` : ''); @@ -13,10 +13,10 @@ const env = { 'DISABLE_STATIC_CONTENT_OPTIMIZATION', 'DISABLE_SYNTHETIC', 'ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL', - 'ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION', 'ENGINE_SERVER', 'FORCE_NATIVE_SHADOW_MODE_FOR_TEST', 'NATIVE_SHADOW', + 'DISABLE_DETACHED_REHYDRATION', ]), LWC_VERSION, NODE_ENV: options.NODE_ENV_FOR_TEST, @@ -28,14 +28,13 @@ 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: [ { resolveImport({ source }) { - if (source === 'test-utils') { - return '/helpers/utils.mjs'; - } else if (source === 'wire-service') { + 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. // `/__wds-outside-root__/${depth}/` === '../'.repeat(depth) @@ -58,13 +57,16 @@ export default { - + `, diff --git a/packages/@lwc/integration-not-karma/configs/hydration.js b/packages/@lwc/integration-not-karma/configs/hydration.js new file mode 100644 index 0000000000..67a2419850 --- /dev/null +++ b/packages/@lwc/integration-not-karma/configs/hydration.js @@ -0,0 +1,11 @@ +// Use native shadow by default in hydration tests; MUST be set before imports +process.env.DISABLE_SYNTHETIC ??= 'true'; +import baseConfig from './base.js'; +import hydrationTestPlugin from './plugins/serve-hydration.js'; + +/** @type {import("@web/test-runner").TestRunnerConfig} */ +export default { + ...baseConfig, + files: ['test-hydration/**/*.spec.js'], + plugins: [...baseConfig.plugins, hydrationTestPlugin], +}; diff --git a/packages/@lwc/integration-not-karma/configs/hydration.mjs b/packages/@lwc/integration-not-karma/configs/hydration.mjs deleted file mode 100644 index eb81e7701a..0000000000 --- a/packages/@lwc/integration-not-karma/configs/hydration.mjs +++ /dev/null @@ -1,19 +0,0 @@ -// 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'; - -/** @type {import("@web/test-runner").TestRunnerConfig} */ -export default { - ...baseConfig, - files: [ - // FIXME: These tests are just symlinks to integration-karma for now so the git diff smaller - 'test-hydration/**/*.spec.js', - // FIXME: hits timeout? - '!test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/index.spec.js', - // FIXME: This uses ENABLE_SYNTHETIC_SHADOW_IN_MIGRATION to detect status, - // we should just use DISABLE_SYNTHETIC instead - '!test-hydration/synthetic-shadow/index.spec.js', - ], - plugins: [...baseConfig.plugins, hydrationTestPlugin], -}; diff --git a/packages/@lwc/integration-not-karma/configs/integration.mjs b/packages/@lwc/integration-not-karma/configs/integration.js similarity index 55% rename from packages/@lwc/integration-not-karma/configs/integration.mjs rename to packages/@lwc/integration-not-karma/configs/integration.js index f277677f86..d5dca29198 100644 --- a/packages/@lwc/integration-not-karma/configs/integration.mjs +++ b/packages/@lwc/integration-not-karma/configs/integration.js @@ -1,5 +1,6 @@ -import baseConfig from './base.mjs'; -import testPlugin from './plugins/serve-integration.mjs'; +import { importMapsPlugin } from '@web/dev-server-import-maps'; +import baseConfig from './base.js'; +import testPlugin from './plugins/serve-integration.js'; /** @type {import("@web/test-runner").TestRunnerConfig} */ export default { @@ -8,23 +9,24 @@ export default { // FIXME: These tests are just symlinks to integration-karma for now so the git diff smaller 'test/**/*.spec.js', - // Cannot reassign properties of module - '!test/api/sanitizeAttribute/index.spec.js', - // Hacky nonsense highly tailored to Karma '!test/custom-elements-registry/index.spec.js', // Logging mismatches '!test/component/LightningElement.addEventListener/index.spec.js', - // Needs clean - '!test/light-dom/multiple-templates/index.spec.js', - '!test/light-dom/style-global/index.spec.js', - '!test/misc/clean-dom/index.spec.js', - '!test/swapping/styles/index.spec.js', - // Implement objectContaining / arrayWithExactContents '!test/profiler/mutation-logging/index.spec.js', + + // Broken in CI? + '!test/lwc-on/index.spec.js', + '!test/api/sanitizeAttribute/index.spec.js', + '!test/template-expressions/errors/index.spec.js', + '!test/template-expressions/smoke-test/index.spec.js', + ], + plugins: [ + ...baseConfig.plugins, + importMapsPlugin({ inject: { importMap: { imports: { lwc: './mocks/lwc.js' } } } }), + testPlugin, ], - plugins: [...baseConfig.plugins, testPlugin], }; diff --git a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js new file mode 100644 index 0000000000..0e63ceed00 --- /dev/null +++ b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js @@ -0,0 +1,167 @@ +import path from 'node:path'; +import vm from 'node:vm'; +import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { rollup } from 'rollup'; +import lwcRollupPlugin from '@lwc/rollup-plugin'; +import { DISABLE_STATIC_CONTENT_OPTIMIZATION, ENGINE_SERVER } from '../../helpers/options.js'; +/** LWC SSR module to use when server-side rendering components. */ +const lwcSsr = await (ENGINE_SERVER + ? // Using import('literal') rather than import(variable) so static analysis tools work + import('@lwc/engine-server') + : import('@lwc/ssr-runtime')); + +lwcSsr.setHooks({ + sanitizeHtmlContent(content) { + return content; + }, +}); + +const ROOT_DIR = path.join(import.meta.dirname, '../..'); +const COMPONENT_NAME = 'x-main'; +const COMPONENT_ENTRYPOINT = 'x/main/main.js'; + +// Like `fs.existsSync` but async +async function exists(path) { + try { + await fs.access(path); + return true; + } catch (_err) { + return false; + } +} + +async function compileModule(input, targetSSR, format) { + const modulesDir = path.join(ROOT_DIR, input.slice(0, -COMPONENT_ENTRYPOINT.length)); + const bundle = await rollup({ + input, + plugins: [ + lwcRollupPlugin({ + targetSSR, + modules: [{ dir: modulesDir }], + experimentalDynamicComponent: { + loader: fileURLToPath(new URL('../../helpers/loader.js', import.meta.url)), + strict: true, + }, + enableDynamicComponents: true, + enableLwcOn: true, + enableStaticContentOptimization: !DISABLE_STATIC_CONTENT_OPTIMIZATION, + experimentalDynamicDirective: true, + }), + ], + + external: ['lwc', '@lwc/ssr-runtime'], + + onwarn(warning, warn) { + // Ignore warnings from our own Rollup plugin + if (warning.plugin !== 'rollup-plugin-lwc-compiler') { + warn(warning); + } + }, + }); + + const { output } = await bundle.generate({ + format, + name: 'Component', + globals: { + lwc: 'LWC', + '@lwc/ssr-runtime': 'LWC', + }, + }); + + return output[0].code; +} + +/** + * This function takes a path to a component definition and a config file and returns the + * SSR-generated markup for the component. It does so by compiling the component and then + * running a script in a separate JS runtime environment to render it. + */ +async function getSsrMarkup(componentEntrypoint, configPath) { + const componentIife = await compileModule(componentEntrypoint, !ENGINE_SERVER, 'iife'); + // To minimize the amount of code in the generated script, ideally we'd do `import Component` + // and delegate the bundling to the loader. However, that's complicated to configure and using + // imports with vm.Script/vm.Module is still experimental, so we use an IIFE for simplicity. + // Additionally, we could import LWC, but the framework requires configuration before each test + // (setHooks/setFeatureFlagForTest), so instead we configure it once in the top-level context + // and inject it as a global variable. + const script = new vm.Script( + `(async () => { + const {default: config} = await import('./${configPath}'); + ${componentIife /* var Component = ... */} + return LWC.renderComponent( + '${COMPONENT_NAME}', + Component, + config.props || {}, + false, + 'sync' + ); + })()`, + { + filename: `[SSR] ${configPath}`, + importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, + } + ); + + return await script.runInContext(vm.createContext({ LWC: lwcSsr })); +} + +async function existsUp(dir, file) { + while (true) { + if (await exists(path.join(dir, file))) return true; + dir = path.join(dir, '..'); + const basename = path.basename(dir); + if (basename === '.') return false; + } +} + +/** + * Hydration test `index.spec.js` files are actually config files, not spec files. + * This function wraps those configs in the test code to be executed. + */ +async function wrapHydrationTest(configPath) { + const { default: config } = await import(path.join(ROOT_DIR, configPath)); + + try { + config.requiredFeatureFlags?.forEach((featureFlag) => { + lwcSsr.setFeatureFlagForTest(featureFlag, true); + }); + + const suiteDir = path.dirname(configPath); + const componentEntrypoint = path.join(suiteDir, COMPONENT_ENTRYPOINT); + // You can add an `.only` file alongside an `index.spec.js` file to make the test focused + const onlyFileExists = await existsUp(suiteDir, '.only'); + const ssrOutput = await getSsrMarkup(componentEntrypoint, configPath); + + return ` + import * as LWC from 'lwc'; + import { runTest } from '/configs/plugins/test-hydration.js'; + runTest( + '/${configPath}?original=1', + '/${componentEntrypoint}', + ${JSON.stringify(ssrOutput) /* escape quotes */}, + ${onlyFileExists} + ); + `; + } finally { + config.requiredFeatureFlags?.forEach((featureFlag) => { + lwcSsr.setFeatureFlagForTest(featureFlag, false); + }); + } +} + +/** @type {import('@web/dev-server-core').Plugin} */ +export default { + async serve(ctx) { + // Hydration test "index.spec.js" files are actually just config files. + // They don't directly define the tests. Instead, when we request the file, + // we wrap it with some boilerplate. That boilerplate must include the config + // file we originally requested, so the ?original query parameter is used + // to return the file unmodified. + if (ctx.path.endsWith('.spec.js') && !ctx.query.original) { + return await wrapHydrationTest(ctx.path.slice(1)); // remove leading / + } else if (ctx.path.endsWith('/' + COMPONENT_ENTRYPOINT)) { + return await compileModule(ctx.path.slice(1) /* remove leading / */, false, 'esm'); + } + }, +}; diff --git a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.mjs b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.mjs deleted file mode 100644 index e89b764b68..0000000000 --- a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.mjs +++ /dev/null @@ -1,246 +0,0 @@ -import path from 'node:path'; -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'; -const lwcSsr = await (ENGINE_SERVER ? import('@lwc/engine-server') : import('@lwc/ssr-runtime')); - -const ROOT_DIR = path.join(import.meta.dirname, '../..'); - -const context = { - LWC: lwcSsr, - moduleOutput: null, -}; - -lwcSsr.setHooks({ - sanitizeHtmlContent(content) { - return content; - }, -}); - -let guid = 0; -const COMPONENT_UNDER_TEST = 'main'; - -// Like `fs.existsSync` but async -async function exists(path) { - try { - await fs.access(path); - return true; - } catch (_err) { - return false; - } -} - -async function getCompiledModule(dir, compileForSSR) { - const bundle = await rollup({ - input: path.join(dir, 'x', COMPONENT_UNDER_TEST, `${COMPONENT_UNDER_TEST}.js`), - plugins: [ - lwcRollupPlugin({ - targetSSR: !!compileForSSR, - modules: [{ dir: path.join(ROOT_DIR, dir) }], - experimentalDynamicComponent: { - loader: 'test-utils', - strict: true, - }, - enableDynamicComponents: true, - enableLwcOn: true, - enableStaticContentOptimization: !DISABLE_STATIC_CONTENT_OPTIMIZATION, - experimentalDynamicDirective: true, - }), - ], - - external: ['lwc', '@lwc/ssr-runtime', 'test-utils', '@test/loader'], // @todo: add ssr modules for test-utils and @test/loader - - onwarn(warning, warn) { - // Ignore warnings from our own Rollup plugin - if (warning.plugin !== 'rollup-plugin-lwc-compiler') { - warn(warning); - } - }, - }); - - const { output } = await bundle.generate({ - format: 'iife', - name: 'Main', - globals: { - lwc: 'LWC', - '@lwc/ssr-runtime': 'LWC', - 'test-utils': 'TestUtils', - }, - }); - - return output[0].code; -} - -function throwOnUnexpectedConsoleCalls(runnable, expectedConsoleCalls = {}) { - // The console is shared between the VM and the main realm. Here we ensure that known warnings - // are ignored and any others cause an explicit error. - const methods = ['error', 'warn', 'log', 'info']; - const originals = {}; - for (const method of methods) { - // eslint-disable-next-line no-console - originals[method] = console[method]; - // eslint-disable-next-line no-console - console[method] = function (error) { - if ( - method === 'warn' && - // This eslint warning is a false positive due to RegExp.prototype.test - // eslint-disable-next-line vitest/no-conditional-tests - /Cannot set property "(inner|outer)HTML"/.test(error?.message) - ) { - return; - } else if ( - expectedConsoleCalls[method]?.some((matcher) => error.message.includes(matcher)) - ) { - return; - } - - throw new Error(`Unexpected console.${method} call: ${error}`); - }; - } - try { - runnable(); - } finally { - Object.assign(console, originals); - } -} - -/** - * This is the function that takes SSR bundle code and test config, constructs a script that will - * run in a separate JS runtime environment with its own global scope. The `context` object - * (defined at the top of this file) is passed in as the global scope for that script. The script - * runs, utilizing the `LWC` object that we've attached to the global scope, it sets a - * new value (the rendered markup) to `globalThis.moduleOutput`, which corresponds to - * `context.moduleOutput in this file's scope. - * - * So, script runs, generates markup, & we get that markup out and return it for use - * in client-side tests. - */ -async function getSsrCode(moduleCode, testConfig, filename, expectedSSRConsoleCalls) { - const script = new vm.Script( - // FIXME: Can these IIFEs be converted to ESM imports? - // No, vm.Script doesn't support that. But might be doable with experimental vm.Module - ` - ${testConfig}; - config = config || {}; - ${moduleCode}; - moduleOutput = LWC.renderComponent( - 'x-${COMPONENT_UNDER_TEST}-${guid++}', - Main, - config.props || {}, - false, - 'sync' - ); - `, - { filename } - ); - - throwOnUnexpectedConsoleCalls(() => { - vm.createContext(context); - script.runInContext(context); - }, expectedSSRConsoleCalls); - - return await context.moduleOutput; -} - -async function getTestConfig(input) { - const bundle = await rollup({ - input, - external: ['lwc', 'test-utils', '@test/loader'], - }); - - const { output } = await bundle.generate({ - format: 'iife', - globals: { - lwc: 'LWC', - 'test-utils': 'TestUtils', - }, - name: 'config', - }); - - const { code } = output[0]; - - return code; -} - -async function existsUp(dir, file) { - while (true) { - if (await exists(path.join(dir, file))) return true; - dir = path.join(dir, '..'); - const basename = path.basename(dir); - if (basename === '.') return false; - } -} - -/** - * Hydration test `index.spec.js` files are actually config files, not spec files. - * This function wraps those configs in the test code to be executed. - */ -async function wrapHydrationTest(filePath) { - const suiteDir = path.dirname(filePath); - - // Wrap all the tests into a describe block with the file stricture name - const describeTitle = path.relative(ROOT_DIR, suiteDir).split(path.sep).join(' '); - - const testCode = await getTestConfig(filePath); - - // Create a temporary module to evaluate the bundled code and extract config properties for test configuration - const configModule = new vm.Script(testCode); - const configContext = { config: {} }; - vm.createContext(configContext); - configModule.runInContext(configContext); - const { expectedSSRConsoleCalls, requiredFeatureFlags } = configContext.config; - - requiredFeatureFlags?.forEach((featureFlag) => { - lwcSsr.setFeatureFlagForTest(featureFlag, true); - }); - - try { - // You can add an `.only` file alongside an `index.spec.js` file to make it `fdescribe()` - const onlyFileExists = await existsUp(suiteDir, '.only'); - - const describeFn = onlyFileExists ? 'describe.only' : 'describe'; - const componentDefCSR = await getCompiledModule(suiteDir, false); - const componentDefSSR = ENGINE_SERVER - ? componentDefCSR - : await getCompiledModule(suiteDir, true); - const ssrOutput = await getSsrCode( - componentDefSSR, - testCode, - path.join(suiteDir, 'ssr.js'), - expectedSSRConsoleCalls - ); - - // FIXME: can we turn these IIFEs into ESM imports? - return ` - import { runTest } from '/helpers/test-hydrate.js'; - import config from '/${filePath}?original=1'; - ${describeFn}("${describeTitle}", () => { - it('test', async () => { - const ssrRendered = ${JSON.stringify(ssrOutput) /* escape quotes */}; - // Component code, IIFE set as Main - ${componentDefCSR}; - return await runTest(ssrRendered, Main, config); - }) - });`; - } finally { - requiredFeatureFlags?.forEach((featureFlag) => { - lwcSsr.setFeatureFlagForTest(featureFlag, false); - }); - } -} - -/** @type {import('@web/dev-server-core').Plugin} */ -export default { - async serve(ctx) { - // Hydration test "index.spec.js" files are actually just config files. - // They don't directly define the tests. Instead, when we request the file, - // we wrap it with some boilerplate. That boilerplate must include the config - // file we originally requested, so the ?original query parameter is used - // to return the file unmodified. - if (ctx.path.endsWith('.spec.js') && !ctx.query.original) { - return await wrapHydrationTest(ctx.path.slice(1)); // remove leading / - } - }, -}; 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 84% 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..e3c14e6db5 100644 --- a/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.mjs +++ b/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js @@ -1,4 +1,5 @@ import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; @@ -7,7 +8,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; @@ -22,7 +23,7 @@ const createRollupPlugin = (input, options) => { // Sourcemaps don't work with Istanbul coverage sourcemap: !process.env.COVERAGE, experimentalDynamicComponent: { - loader: 'test-utils', + loader: fileURLToPath(new URL('../../helpers/dynamic-loader', import.meta.url)), strict: true, }, enableDynamicComponents: true, @@ -83,9 +84,15 @@ const transform = async (ctx) => { cache, plugins: [customLwcRollupPlugin], - // Rollup should not attempt to resolve the engine and the test utils, Karma takes care of injecting it - // globally in the page before running the tests. - external: ['lwc', 'wire-service', 'test-utils', '@test/loader'], + external: [ + 'lwc', + 'wire-service', + // Some helper files export functions that mutate a global state. The setup file calls + // some of those functions and does not get bundled. Including the helper files in the + // bundle would create a separate global state, causing tests to fail. We don't need to + // mark _all_ helpers as external, but we do anyway for ease of maintenance. + /\/helpers\/\w+\.js$/, + ], onwarn(warning, warn) { // Ignore warnings from our own Rollup plugin diff --git a/packages/@lwc/integration-not-karma/configs/plugins/test-hydration.js b/packages/@lwc/integration-not-karma/configs/plugins/test-hydration.js new file mode 100644 index 0000000000..96d0371034 --- /dev/null +++ b/packages/@lwc/integration-not-karma/configs/plugins/test-hydration.js @@ -0,0 +1,77 @@ +import * as LWC from 'lwc'; +import { spyConsole } from '../../helpers/console'; +import { setHooks } from '../../helpers/hooks'; + +setHooks({ sanitizeHtmlContent: (content) => content }); + +function parseStringToDom(html) { + return Document.parseHTMLUnsafe(html).body.firstChild; +} + +function appendTestTarget(ssrText) { + const div = document.createElement('div'); + const testTarget = parseStringToDom(ssrText); + div.appendChild(testTarget); + document.body.appendChild(div); + return div; +} + +function setFeatureFlags(requiredFeatureFlags, value) { + requiredFeatureFlags?.forEach((featureFlag) => { + LWC.setFeatureFlagForTest(featureFlag, value); + }); +} + +// Must be sync to properly register tests; async behavior can happen in before/after blocks +export function runTest(configPath, componentPath, ssrRendered, focused) { + const test = focused ? it.only : it; + const description = new URL(configPath, location.href).pathname; + let consoleSpy; + let testConfig; + let Component; + + beforeAll(async () => { + testConfig = await import(configPath); + Component = await import(componentPath); + setFeatureFlags(testConfig.requiredFeatureFlags, true); + }); + + beforeEach(async () => { + consoleSpy = spyConsole(); + }); + + afterEach(() => { + consoleSpy.reset(); + }); + + afterAll(() => { + setFeatureFlags(testConfig.requiredFeatureFlags, false); + }); + + test(description, async () => { + const container = appendTestTarget(ssrRendered); + const selector = container.firstChild.tagName.toLowerCase(); + let target = container.querySelector(selector); + + if (testConfig.test) { + const snapshot = testConfig.snapshot ? testConfig.snapshot(target) : {}; + + const props = testConfig.props || {}; + const clientProps = testConfig.clientProps || props; + + LWC.hydrateComponent(target, Component, clientProps); + + // let's select again the target, it should be the same elements as in the snapshot + target = container.querySelector(selector); + await testConfig.test(target, snapshot, consoleSpy.calls); + } else if (testConfig.advancedTest) { + await testConfig.advancedTest(target, { + Component, + hydrateComponent: LWC.hydrateComponent.bind(LWC), + consoleSpy, + container, + selector, + }); + } + }); +} 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 75% rename from packages/@lwc/integration-not-karma/helpers/constants.mjs rename to packages/@lwc/integration-not-karma/helpers/constants.js index 0dac9715b3..50454f2362 100644 --- a/packages/@lwc/integration-not-karma/helpers/constants.mjs +++ b/packages/@lwc/integration-not-karma/helpers/constants.js @@ -1,7 +1,7 @@ -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, +export const // These values are based on the API versions in @lwc/shared/api-version + LOWERCASE_SCOPE_TOKENS = API_VERSION >= 59, USE_COMMENTS_FOR_FRAGMENT_BOOKENDS = API_VERSION >= 60, USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS = API_VERSION >= 60, DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION = API_VERSION >= 60, diff --git a/packages/@lwc/integration-not-karma/helpers/dynamic-loader.js b/packages/@lwc/integration-not-karma/helpers/dynamic-loader.js new file mode 100644 index 0000000000..b61af17f11 --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/dynamic-loader.js @@ -0,0 +1,9 @@ +// Helpers for testing lwc:dynamic +const register = new Map(); +/** + * Called by compiled components to, well, load another component. The path to this file is + * specified by the `experimentalDynamicComponent.loader` rollup plugin option. + */ +export const load = async (id) => await Promise.resolve(register.get(id)); +export const registerForLoad = (name, Ctor) => register.set(name, Ctor); +export const clearRegister = () => register.clear(); 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/jasmine.js b/packages/@lwc/integration-not-karma/helpers/jasmine.js new file mode 100644 index 0000000000..8a459204af --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/jasmine.js @@ -0,0 +1,37 @@ +import { spyOn, fn } from '@vitest/spy'; + +/** + * Adds the jasmine interfaces we use in the Karma tests to a Vitest spy. + * Should ultimately be removed and tests updated to use Vitest spies. + * @param {import('@vitest/spy').MockInstance} + */ +function jasmineSpyAdapter(spy) { + Object.defineProperties(spy, { + and: { get: () => spy }, + calls: { get: () => spy.mock.calls }, + returnValue: { value: () => spy.mockReturnValue() }, + // calling mockImplementation() with nothing restores the original + callThrough: { value: () => spy.mockImplementation() }, + callFake: { value: (impl) => spy.mockImplementation(impl) }, + }); + + Object.defineProperties(spy.mock.calls, { + // Must be non-enumerable for equality checks to work on array literal expected values + allArgs: { value: () => spy.mock.calls }, + count: { value: () => spy.mock.calls.length }, + reset: { value: () => spy.mockReset() }, + argsFor: { value: (index) => spy.mock.calls.at(index) }, + }); + + return spy; +} + +export const jasmineSpyOn = (object, prop) => jasmineSpyAdapter(spyOn(object, prop)); +export const jasmine = { + any: expect.any, + arrayWithExactContents: () => { + throw new Error('TODO: jasmine.arrayWithExactContents'); + }, + createSpy: (name, impl) => jasmineSpyAdapter(fn(impl)), + objectContaining: expect.objectContaining, +}; 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 92% rename from packages/@lwc/integration-not-karma/helpers/options.mjs rename to packages/@lwc/integration-not-karma/helpers/options.js index a18ae6df4a..7124677ef4 100644 --- a/packages/@lwc/integration-not-karma/helpers/options.mjs +++ b/packages/@lwc/integration-not-karma/helpers/options.js @@ -26,14 +26,12 @@ export const DISABLE_STATIC_CONTENT_OPTIMIZATION = Boolean( process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION ); -export const ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION = Boolean( - process.env.ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION -); - export const DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE = Boolean( process.env.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE ); +export const DISABLE_DETACHED_REHYDRATION = Boolean(process.env.DISABLE_DETACHED_REHYDRATION); + export const ENGINE_SERVER = Boolean(process.env.ENGINE_SERVER); // --- Test config --- // @@ -44,8 +42,6 @@ export const API_VERSION = process.env.API_VERSION export const NODE_ENV_FOR_TEST = process.env.NODE_ENV_FOR_TEST || 'development'; -export const GREP = process.env.GREP; - export const NATIVE_SHADOW = DISABLE_SYNTHETIC || FORCE_NATIVE_SHADOW_MODE_FOR_TEST; /** Unique directory name that encodes the flags that the tests were executed with. */ @@ -56,12 +52,12 @@ export const COVERAGE_DIR_FOR_OPTIONS = DISABLE_SYNTHETIC, DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER, ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL, - ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION, FORCE_NATIVE_SHADOW_MODE_FOR_TEST, LEGACY_BROWSERS, NODE_ENV_FOR_TEST, DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE, ENGINE_SERVER, + DISABLE_DETACHED_REHYDRATION, }) .filter(([, val]) => val) .map(([key, val]) => `${key}=${val}`) diff --git a/packages/@lwc/integration-not-karma/helpers/reporting-control.js b/packages/@lwc/integration-not-karma/helpers/reporting-control.js new file mode 100644 index 0000000000..390b2b6679 --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/reporting-control.js @@ -0,0 +1,18 @@ +import { __unstable__ReportingControl } from 'lwc'; + +/** + * + * @param dispatcher + * @param runtimeEvents List of runtime events to filter by. If no list is provided, all events will be dispatched. + */ +export function attachReportingControlDispatcher(dispatcher, runtimeEvents) { + __unstable__ReportingControl.attachDispatcher((eventName, payload) => { + if (!runtimeEvents || runtimeEvents.includes(eventName)) { + dispatcher(eventName, payload); + } + }); +} + +export function detachReportingControlDispatcher() { + __unstable__ReportingControl.detachDispatcher(); +} diff --git a/packages/@lwc/integration-not-karma/helpers/setup.mjs b/packages/@lwc/integration-not-karma/helpers/setup.js similarity index 59% rename from packages/@lwc/integration-not-karma/helpers/setup.mjs rename to packages/@lwc/integration-not-karma/helpers/setup.js index 5098985cd3..691332359c 100644 --- a/packages/@lwc/integration-not-karma/helpers/setup.mjs +++ b/packages/@lwc/integration-not-karma/helpers/setup.js @@ -1,14 +1,10 @@ // This import ensures that the global `Mocha` object is present for mutation. import { JestAsymmetricMatchers, JestChaiExpect, JestExtend } from '@vitest/expect'; 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 { initSignals } from './signals.js'; -// FIXME: As a relic of the Karma tests, some test files rely on the global object, -// rather than importing from `test-utils`. -window.TestUtils = TestUtils; +initSignals(); // allows using expect.extend instead of chai.use to extend plugins chai.use(JestExtend); @@ -18,46 +14,8 @@ chai.use(JestChaiExpect); chai.use(JestAsymmetricMatchers); // add our custom matchers chai.use(registerCustomMatchers); - -/** - * Adds the jasmine interfaces we use in the Karma tests to a Vitest spy. - * Should ultimately be removed and tests updated to use Vitest spies. - * @param {import('@vitest/spy').MockInstance} - */ -function jasmineSpyAdapter(spy) { - Object.defineProperties(spy, { - and: { get: () => spy }, - calls: { get: () => spy.mock.calls }, - returnValue: { value: () => spy.mockReturnValue() }, - // calling mockImplementation() with nothing restores the original - callThrough: { value: () => spy.mockImplementation() }, - callFake: { value: (impl) => spy.mockImplementation(impl) }, - }); - - Object.defineProperties(spy.mock.calls, { - // Must be non-enumerable for equality checks to work on array literal expected values - allArgs: { value: () => spy.mock.calls }, - count: { value: () => spy.mock.calls.length }, - reset: { value: () => spy.mockReset() }, - argsFor: { value: (index) => spy.mock.calls.at(index) }, - }); - - return spy; -} - // expose so we don't need to import `expect` in every test file globalThis.expect = chai.expect; -// Expose globals for karma compat -globalThis.LWC = LWC; -globalThis.spyOn = (object, prop) => jasmineSpyAdapter(spyOn(object, prop)); -globalThis.jasmine = { - any: expect.any, - arrayWithExactContents: () => { - throw new Error('TODO: jasmine.arrayWithExactContents'); - }, - createSpy: (name, impl) => jasmineSpyAdapter(fn(impl)), - objectContaining: expect.objectContaining, -}; /** * `@web/test-runner-mocha`'s autorun.js file inlines its own copy of mocha, and there's no direct @@ -106,6 +64,7 @@ hijackGlobal('afterEach', (afterEach) => { // FIXME: Boost test speed by moving this to only files that need it // Ensure the DOM is in a clean state document.body.replaceChildren(); + document.head.replaceChildren(); }); }); diff --git a/packages/@lwc/integration-not-karma/helpers/signals.mjs b/packages/@lwc/integration-not-karma/helpers/signals.js similarity index 67% rename from packages/@lwc/integration-not-karma/helpers/signals.mjs rename to packages/@lwc/integration-not-karma/helpers/signals.js index 40a45eada1..f3dd96ed28 100644 --- a/packages/@lwc/integration-not-karma/helpers/signals.mjs +++ b/packages/@lwc/integration-not-karma/helpers/signals.js @@ -1,7 +1,10 @@ import { setTrustedSignalSet } from 'lwc'; const signalValidator = new WeakSet(); -setTrustedSignalSet(signalValidator); + +export function initSignals() { + setTrustedSignalSet(signalValidator); +} export function addTrustedSignal(signal) { signalValidator.add(signal); diff --git a/packages/@lwc/integration-not-karma/helpers/test-hydrate.js b/packages/@lwc/integration-not-karma/helpers/test-hydrate.js deleted file mode 100644 index 5b65bdc6c2..0000000000 --- a/packages/@lwc/integration-not-karma/helpers/test-hydrate.js +++ /dev/null @@ -1,60 +0,0 @@ -import * as LWC from 'lwc'; - -window.TestUtils.setHooks({ - sanitizeHtmlContent: (content) => content, -}); - -function parseStringToDom(html) { - return Document.parseHTMLUnsafe(html).body.firstChild; -} - -function appendTestTarget(ssrText) { - const div = document.createElement('div'); - const testTarget = parseStringToDom(ssrText); - div.appendChild(testTarget); - document.body.appendChild(div); - return div; -} - -function setFeatureFlags(requiredFeatureFlags, value) { - requiredFeatureFlags?.forEach((featureFlag) => { - LWC.setFeatureFlagForTest(featureFlag, value); - }); -} - -async function runTest(ssrRendered, Component, testConfig) { - const container = appendTestTarget(ssrRendered); - const selector = container.firstChild.tagName.toLowerCase(); - let target = container.querySelector(selector); - - let testResult; - const consoleSpy = window.TestUtils.spyConsole(); - setFeatureFlags(testConfig.requiredFeatureFlags, true); - - if (testConfig.test) { - const snapshot = testConfig.snapshot ? testConfig.snapshot(target) : {}; - - const props = testConfig.props || {}; - const clientProps = testConfig.clientProps || props; - - LWC.hydrateComponent(target, Component, clientProps); - - // let's select again the target, it should be the same elements as in the snapshot - target = container.querySelector(selector); - testResult = await testConfig.test(target, snapshot, consoleSpy.calls); - } else if (testConfig.advancedTest) { - testResult = await testConfig.advancedTest(target, { - Component, - hydrateComponent: LWC.hydrateComponent.bind(LWC), - consoleSpy, - container, - selector, - }); - } - - consoleSpy.reset(); - - return testResult; -} - -export { runTest }; diff --git a/packages/@lwc/integration-not-karma/helpers/utils.mjs b/packages/@lwc/integration-not-karma/helpers/utils.js similarity index 74% rename from packages/@lwc/integration-not-karma/helpers/utils.mjs rename to packages/@lwc/integration-not-karma/helpers/utils.js index 5ebdd95dde..ef6fde2c77 100644 --- a/packages/@lwc/integration-not-karma/helpers/utils.mjs +++ b/packages/@lwc/integration-not-karma/helpers/utils.js @@ -1,29 +1,6 @@ /* * An as yet uncategorized mishmash of helpers, relics of Karma */ -import * as LWC from 'lwc'; -import { - ariaAttributes, - ariaProperties, - ariaPropertiesMapping, - nonPolyfilledAriaProperties, - nonStandardAriaProperties, -} from './aria.mjs'; -import { setHooks, getHooks } from './hooks.mjs'; -import { spyConsole } from './console.mjs'; -import { - DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, - ENABLE_ELEMENT_INTERNALS_AND_FACE, - ENABLE_THIS_DOT_HOST_ELEMENT, - ENABLE_THIS_DOT_STYLE, - IS_SYNTHETIC_SHADOW_LOADED, - LOWERCASE_SCOPE_TOKENS, - TEMPLATE_CLASS_NAME_OBJECT_BINDING, - USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, - USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, - USE_LIGHT_DOM_SLOT_FORWARDING, -} from './constants.mjs'; -import { addTrustedSignal } from './signals.mjs'; // Listen for errors thrown directly by the callback function directErrorListener(callback) { @@ -63,30 +40,13 @@ function windowErrorListener(callback) { // 2) We're using native lifecycle callbacks, so the error is thrown asynchronously and can // only be caught with window.addEventListener('error') // - Note native lifecycle callbacks are all thrown asynchronously. -function customElementCallbackReactionErrorListener(callback) { +export function customElementCallbackReactionErrorListener(callback) { return lwcRuntimeFlags.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE ? directErrorListener(callback) : windowErrorListener(callback); } -/** - * - * @param dispatcher - * @param runtimeEvents List of runtime events to filter by. If no list is provided, all events will be dispatched. - */ -function attachReportingControlDispatcher(dispatcher, runtimeEvents) { - LWC.__unstable__ReportingControl.attachDispatcher((eventName, payload) => { - if (!runtimeEvents || runtimeEvents.includes(eventName)) { - dispatcher(eventName, payload); - } - }); -} - -function detachReportingControlDispatcher() { - LWC.__unstable__ReportingControl.detachDispatcher(); -} - -function extractDataIds(root) { +export function extractDataIds(root) { const nodes = {}; function processElement(elm) { @@ -120,7 +80,7 @@ function extractDataIds(root) { return nodes; } -function extractShadowDataIds(shadowRoot) { +export function extractShadowDataIds(shadowRoot) { const nodes = {}; // Add the shadow root here even if they don't have [data-id] attributes. This reference is @@ -140,36 +100,24 @@ function extractShadowDataIds(shadowRoot) { return nodes; } -let register = {}; -function load(id) { - return Promise.resolve(register[id]); -} - -function registerForLoad(name, Ctor) { - register[name] = Ctor; -} -function clearRegister() { - register = {}; -} - // #986 - childNodes on the host element returns a fake shadow comment node on IE11 for debugging purposes. This method // filters this node. -function getHostChildNodes(host) { +export function getHostChildNodes(host) { return Array.prototype.slice.call(host.childNodes).filter(function (n) { return !(n.nodeType === Node.COMMENT_NODE && n.tagName.startsWith('#shadow-root')); }); } -function isSyntheticShadowRootInstance(sr) { +export function isSyntheticShadowRootInstance(sr) { return Boolean(sr && sr.synthetic); } -function isNativeShadowRootInstance(sr) { +export function isNativeShadowRootInstance(sr) { return Boolean(sr && !sr.synthetic); } // Keep traversing up the prototype chain until a property descriptor is found -function getPropertyDescriptor(object, prop) { +export function getPropertyDescriptor(object, prop) { do { const descriptor = Object.getOwnPropertyDescriptor(object, prop); if (descriptor) { @@ -216,13 +164,13 @@ function stringifyArg(arg) { } } -const expectConsoleCalls = createExpectConsoleCallsFunc(false); -const expectConsoleCallsDev = createExpectConsoleCallsFunc(true); +export const expectConsoleCalls = createExpectConsoleCallsFunc(false); +export const expectConsoleCallsDev = createExpectConsoleCallsFunc(true); // Utility to handle unhandled rejections or errors without allowing Jasmine to handle them first. // Captures both onunhandledrejection and onerror events, since you might want both depending on // native vs synthetic lifecycle timing differences. -function catchUnhandledRejectionsAndErrors(onUnhandledRejectionOrError) { +export function catchUnhandledRejectionsAndErrors(onUnhandledRejectionOrError) { let originalOnError; const onError = (e) => { @@ -257,7 +205,7 @@ function catchUnhandledRejectionsAndErrors(onUnhandledRejectionOrError) { // Succeeds if the given DOM element is equivalent to the given HTML in terms of nodes and elements. This is // basically the same as `expect(element.outerHTML).toBe(html)` except that it works despite bugs in synthetic shadow. -function expectEquivalentDOM(element, html) { +export function expectEquivalentDOM(element, html) { const fragment = Document.parseHTMLUnsafe(html); // When the fragment is parsed, the string "abc" is considered one text node. Whereas the engine @@ -314,41 +262,3 @@ function expectEquivalentDOM(element, html) { expectEquivalent(element, fragment.body.firstChild); } - -export { - clearRegister, - extractDataIds, - extractShadowDataIds, - getHostChildNodes, - isNativeShadowRootInstance, - isSyntheticShadowRootInstance, - load, - registerForLoad, - getHooks, - setHooks, - spyConsole, - customElementCallbackReactionErrorListener, - ariaPropertiesMapping, - ariaProperties, - ariaAttributes, - nonStandardAriaProperties, - nonPolyfilledAriaProperties, - getPropertyDescriptor, - attachReportingControlDispatcher, - detachReportingControlDispatcher, - IS_SYNTHETIC_SHADOW_LOADED, - expectConsoleCalls, - expectConsoleCallsDev, - catchUnhandledRejectionsAndErrors, - addTrustedSignal, - expectEquivalentDOM, - LOWERCASE_SCOPE_TOKENS, - USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, - USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, - DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, - ENABLE_ELEMENT_INTERNALS_AND_FACE, - USE_LIGHT_DOM_SLOT_FORWARDING, - ENABLE_THIS_DOT_HOST_ELEMENT, - ENABLE_THIS_DOT_STYLE, - TEMPLATE_CLASS_NAME_OBJECT_BINDING, -}; 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 3d89b08686..9496167ec3 100644 --- a/packages/@lwc/integration-not-karma/package.json +++ b/packages/@lwc/integration-not-karma/package.json @@ -1,23 +1,26 @@ { "name": "@lwc/integration-not-karma", "private": true, - "version": "8.21.2", + "version": "8.22.2", + "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.2", - "@lwc/engine-dom": "8.21.2", - "@lwc/engine-server": "8.21.2", - "@lwc/rollup-plugin": "8.21.2", - "@lwc/synthetic-shadow": "8.21.2", + "@lwc/compiler": "8.22.2", + "@lwc/engine-dom": "8.22.2", + "@lwc/engine-server": "8.22.2", + "@lwc/rollup-plugin": "8.22.2", + "@lwc/synthetic-shadow": "8.22.2", "@types/chai": "^5.2.2", - "@types/jasmine": "^5.1.8", + "@types/jasmine": "^5.1.9", + "@vitest/spy": "^3.2.4", + "@web/dev-server-import-maps": "^0.2.1", "@web/dev-server-rollup": "^0.6.4", "@web/test-runner": "^0.20.2", - "chai": "^5.2.1" + "chai": "^6.0.1" }, "volta": { "extends": "../../../package.json" 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..7fe011220f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/attributes/falsy-mismatch/index.spec.js @@ -0,0 +1,42 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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]); + } + + 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..572e73f46d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/context/index.spec.js @@ -0,0 +1,103 @@ +import { expectConsoleCalls } from '../../helpers/utils.js'; + +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) + 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..63eb4fdac4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/errors/already-hydrated/index.spec.js @@ -0,0 +1,15 @@ +import { expectConsoleCalls } from '../../../helpers/utils.js'; + +export default { + advancedTest(target, { Component, hydrateComponent, consoleSpy }) { + hydrateComponent(target, Component, {}); + + hydrateComponent(target, Component, {}); + + const consoleCalls = consoleSpy.calls; + + 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..3ae85ba4db --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/inner-outer-html/index.spec.js @@ -0,0 +1,18 @@ +import { extractDataIds } from '../../helpers/utils.js'; + +export default { + props: {}, + advancedTest(target, { consoleSpy }) { + const ids = Object.entries(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..c2aad762a0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/light-dom/scoped-styles/deduped-scoped-styles/index.spec.js @@ -0,0 +1,11 @@ +import { expectConsoleCalls } from '../../../../helpers/utils.js'; + +export default { + test(target, snapshot, consoleCalls) { + // W-19087941: Expect no errors or warnings, hydration or otherwise + 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..144234c381 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-compatibility/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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'); + + 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..d1de3fb3a4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/attrs-expression/index.spec.js @@ -0,0 +1,30 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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'); + + 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..9f30773d04 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/dynamic-different/index.spec.js @@ -0,0 +1,31 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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); + + 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..d314088097 --- /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,34 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +// 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); + + 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..65b3eb3256 --- /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,34 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +// 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); + + 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..56d60f9fc7 --- /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,28 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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); + + 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..238a20847b --- /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,30 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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(''); + + 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..ca71555d77 --- /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,30 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..b986510723 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/empty-string/index.spec.js @@ -0,0 +1,24 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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); + + 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..300c373aae --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-client/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..7bec4446a3 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/extra-class-from-server/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..366280e61f --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/only-present-in-ssr/index.spec.js @@ -0,0 +1,24 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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; + 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..920742d515 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/class-attr/same-different-order/index.spec.js @@ -0,0 +1,39 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..2c2b60f320 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/comment-instead-of-text/index.spec.js @@ -0,0 +1,30 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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); + + 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..ef8ecc792e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/different-lwc-inner-html/index.spec.js @@ -0,0 +1,39 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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'); + + 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..1d39e94e19 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/display-errors-attrs-class-style/index.spec.js @@ -0,0 +1,38 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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'); + + 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..74ddba5889 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/dynamic-component/index.spec.js @@ -0,0 +1,30 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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(); + + 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..bd82a6efb4 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/element-instead-of-textNode/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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); + + 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..a83893a26d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-comment/index.spec.js @@ -0,0 +1,30 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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'); + + 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..1a8e675d19 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/favors-client-side-text/index.spec.js @@ -0,0 +1,38 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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) { + expectConsoleCallsDev(consoleCalls, { + error: [], + warn: [ + 'Hydration text content mismatch on: #text - rendered on server: hello! - expected on client: bye!', + ], + }); + } else { + 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..1ecc068a05 --- /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,37 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..d59747490a --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/attr/index.spec.js @@ -0,0 +1,24 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..2ceea2c5b7 --- /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,36 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..3124ee87e7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/class/index.spec.js @@ -0,0 +1,24 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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' + ); + + 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..e2c187c30d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/host-mutation-in-connected-callback/style/index.spec.js @@ -0,0 +1,22 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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'); + + 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..04b4d610c9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/foreach/index.spec.js @@ -0,0 +1,28 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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); + + 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..12d190bc15 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/invalid-number-of-nodes/if-true/index.spec.js @@ -0,0 +1,39 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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) { + 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 { + 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..4dceba963c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/sibling-issue/index.spec.js @@ -0,0 +1,29 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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(); + + 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..4e0aa56496 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/different-priority/index.spec.js @@ -0,0 +1,34 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;' + ); + + 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..8520604f94 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-client/index.spec.js @@ -0,0 +1,34 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;' + ); + + 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..bdf60b7c45 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/extra-from-server/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;'); + + 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..40786d91c2 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/computed/same-different-order/index.spec.js @@ -0,0 +1,33 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;' + ); + + 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..6ccc8fb235 --- /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,30 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..745cffe63d --- /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,30 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;'); + + 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..8566cc6778 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/different-priority/index.spec.js @@ -0,0 +1,34 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;' + ); + + 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..fb56c341e9 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-client/index.spec.js @@ -0,0 +1,34 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;' + ); + + 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..6d4cb3bedf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/extra-from-server/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;'); + + 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..4f60a1854d --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/style-attr/static/same-different-order/index.spec.js @@ -0,0 +1,41 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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;' + ); + + 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..5067cbd985 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/text-instead-of-comment/index.spec.js @@ -0,0 +1,30 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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); + + 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..0fa8c76759 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test-hydration/mismatches/textNode-instead-of-element/index.spec.js @@ -0,0 +1,32 @@ +import { expectConsoleCallsDev } from '../../../helpers/utils.js'; + +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); + + 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..26b5a4cfaf --- /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,21 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..26b5a4cfaf --- /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,21 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..28a27b49db --- /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,27 @@ +import { expectConsoleCallsDev } from '../../../../helpers/utils.js'; + +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); + + 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..39ae491fdd --- /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,20 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..39ae491fdd --- /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,20 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..39ae491fdd --- /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,20 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..39ae491fdd --- /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,20 @@ +import { expectConsoleCallsDev } from '../../../../../helpers/utils.js'; + +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); + + 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..65e4bdee63 --- /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,31 @@ +import { expectConsoleCallsDev } from '../../../../../../helpers/utils.js'; + +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); + + 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..fca2e74f7e --- /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,31 @@ +import { expectConsoleCallsDev } from '../../../../../../helpers/utils.js'; + +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); + + 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..73d6350229 --- /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,39 @@ +import { expectConsoleCallsDev } from '../../../../../../helpers/utils.js'; + +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'"); + + 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..4d1607e176 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/event-post-dispatch.spec.js @@ -0,0 +1,87 @@ +// Inspired from WPT: +// https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-post-dispatch.html + +import { createElement } from 'lwc'; +import Container from 'x/container'; +import { extractDataIds } from '../../../helpers/utils.js'; + +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..aa9c81d892 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/shadow-dom/event-in-shadow-tree/propagation.spec.js @@ -0,0 +1,629 @@ +// Inspired from WPT: +// https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-inside-shadow-tree.html + +import { createElement } from 'lwc'; +import Container from 'x/container'; +import { extractDataIds } from '../../../helpers/utils.js'; + +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..373a01d248 --- /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 Grandparent from 'x/grandparent'; +import { extractDataIds } from '../../../helpers/utils.js'; + +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..ae99c63ed7 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js @@ -0,0 +1,249 @@ +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'; +import { jasmine } from '../../../helpers/jasmine.js'; + +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..abfc1627cd --- /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 '../../../../../helpers/signals.js'; + +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..d56372c37c --- /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 '../../../../../helpers/signals.js'; + +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/signal/untrusted/index.spec.js b/packages/@lwc/integration-not-karma/test/signal/untrusted/index.spec.js new file mode 100644 index 0000000000..f7c4ee9aaf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/untrusted/index.spec.js @@ -0,0 +1,126 @@ +import { createElement, setFeatureFlagForTest } from 'lwc'; + +import Test from 'x/test'; +import { Signal } from 'x/signal'; +import { spyConsole } from '../../../helpers/console.js'; + +const createElementSignalAndInsertIntoDom = async (object) => { + const elm = createElement('x-test', { is: Test }); + elm.object = object; + document.body.appendChild(elm); + await Promise.resolve(); + return elm; +}; + +describe('signal reaction in lwc', () => { + let consoleSpy; + + beforeAll(() => setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true)); + afterAll(() => setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false)); + beforeEach(() => (consoleSpy = spyConsole())); + afterEach(() => consoleSpy.reset()); + + describe('with trusted signal set', () => { + describe('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION is enabled', () => { + beforeAll(() => setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true)); + afterAll(() => setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false)); + it('will not warn if rendering non-signal objects ', async () => { + const elm = await createElementSignalAndInsertIntoDom({ + value: 'non signal value', + }); + expect(consoleSpy.calls.warn.length).toEqual(0); + expect(elm.shadowRoot.textContent).toBe('non signal value'); + }); + it('will not warn if rendering signal objects', async () => { + const signal = new Signal('signal value'); + const elm = await createElementSignalAndInsertIntoDom(signal); + expect(consoleSpy.calls.warn.length).toEqual(0); + signal.value = 'new signal value'; + await Promise.resolve(); + expect(elm.shadowRoot.textContent).toBe('new signal value'); + }); + }); + + describe('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION is disabled', () => { + beforeAll(() => + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false) + ); + it('will not warn if rendering non-signal objects', async () => { + const elm = await createElementSignalAndInsertIntoDom({ + value: 'non signal value', + }); + expect(consoleSpy.calls.warn.length).toEqual(0); + expect(elm.shadowRoot.textContent).toBe('non signal value'); + }); + it('will not warn if rendering signal objects', async () => { + const signal = new Signal('signal value'); + const elm = await createElementSignalAndInsertIntoDom(signal); + expect(consoleSpy.calls.warn.length).toEqual(0); + signal.value = 'new signal value'; + await Promise.resolve(); + expect(elm.shadowRoot.textContent).toBe('new signal value'); + }); + }); + }); + + describe('without trusted signal set', () => { + beforeAll(() => globalThis.__lwcResetTrustedSignalsSetForTest()); + describe('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION is enabled', () => { + beforeAll(() => setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true)); + afterAll(() => setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false)); + /** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedSignals set. + * However, this resulted in a bug as all object values were + * being considered signals in environments where the trustedSignals + * set had not been defined. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ + it('will warn if rendering non-signal objects ', async () => { + const elm = await createElementSignalAndInsertIntoDom({ + value: 'non signal value', + }); + expect(consoleSpy.calls.warn[0][0].message).toContain( + 'Attempted to subscribe to an object that has the shape of a signal but received the following error: TypeError: signal.subscribe is not a function' + ); + expect(elm.shadowRoot.textContent).toBe('non signal value'); + }); + it('will not warn if rendering signal objects', async () => { + const signal = new Signal('signal value'); + const elm = await createElementSignalAndInsertIntoDom(signal); + expect(consoleSpy.calls.warn.length).toEqual(0); + signal.value = 'new signal value'; + await Promise.resolve(); + expect(elm.shadowRoot.textContent).toBe('new signal value'); + }); + }); + + describe('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION is disabled', () => { + beforeAll(() => + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false) + ); + it('will not warn if rendering non-signal objects', async () => { + const elm = await createElementSignalAndInsertIntoDom({ + value: 'non signal value', + }); + expect(consoleSpy.calls.warn.length).toEqual(0); + expect(elm.shadowRoot.textContent).toBe('non signal value'); + }); + /** + * Signals are designed to be used where trustedSignalSet has been defined via setTrustedSignalSet + * This is because checking against the set is an efficient way to determine if an object is a Signal + * This is acceptable as Signals is an internal API and designed to work where setTrustedSignalSet has been used. + * Because of this, the signal value does not change here. + * See #5347 for context. + */ + it('will not warn if rendering signal objects but it will not react', async () => { + const signal = new Signal('signal value'); + const elm = await createElementSignalAndInsertIntoDom(signal); + expect(consoleSpy.calls.warn.length).toEqual(0); + signal.value = 'new signal value'; + await Promise.resolve(); + expect(elm.shadowRoot.textContent).toBe('signal value'); + }); + }); + }); +}); diff --git a/packages/@lwc/integration-not-karma/test/signal/untrusted/x/signal/signal.js b/packages/@lwc/integration-not-karma/test/signal/untrusted/x/signal/signal.js new file mode 100644 index 0000000000..d56372c37c --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/untrusted/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 '../../../../../helpers/signals.js'; + +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/signal/untrusted/x/test/test.html b/packages/@lwc/integration-not-karma/test/signal/untrusted/x/test/test.html new file mode 100644 index 0000000000..0dca7be2cf --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/untrusted/x/test/test.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/signal/untrusted/x/test/test.js b/packages/@lwc/integration-not-karma/test/signal/untrusted/x/test/test.js new file mode 100644 index 0000000000..cff13b0901 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/signal/untrusted/x/test/test.js @@ -0,0 +1,5 @@ +import { LightningElement, api } from 'lwc'; + +export default class Test extends LightningElement { + @api object; +} 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..622988db12 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/spread/index.spec.js @@ -0,0 +1,118 @@ +import { createElement } from 'lwc'; +import Test from 'x/test'; +import { jasmine, jasmineSpyOn as spyOn } from '../../helpers/jasmine.js'; +import { getHooks, setHooks } from '../../helpers/hooks.js'; + +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..4dac801e29 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/static-content/index.spec.js @@ -0,0 +1,867 @@ +import { createElement } from 'lwc'; +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'; +import { jasmine } from '../../helpers/jasmine.js'; +import { LOWERCASE_SCOPE_TOKENS } from '../../helpers/constants.js'; +import { extractDataIds } from '../../helpers/utils.js'; + +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..d9f1a21e00 --- /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 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'; +import { extractDataIds } from '../../../helpers/utils.js'; + +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..d68723e254 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/disable-synthetic-shadow/index.spec.js @@ -0,0 +1,33 @@ +import { createElement, setFeatureFlagForTest } from 'lwc'; +import Component from 'x/component'; +import { IS_SYNTHETIC_SHADOW_LOADED } from '../../../helpers/constants.js'; +import { isSyntheticShadowRootInstance } from '../../../helpers/utils.js'; + +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..ae2c315c4e --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/synthetic-shadow/element-api/element-api.spec.js @@ -0,0 +1,417 @@ +import { createElement } from 'lwc'; + +import Container from 'x/container'; +import ParentSpecialized from 'x/parentSpecialized'; +import { jasmineSpyOn as spyOn } from '../../../helpers/jasmine.js'; + +/* +
        + +

        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