(@wdio/browser-runner): add StencilJS component testing support (#10927)
* (@wdio/browser-runner): add StencilJS component testing support * update unit tests * improve starter template * make use of common stencil dep * add docs * fix tests * clean up interface * debug build in windows * increase e2e test timeout * skip scroll tests in Windows
This commit is contained in:
parent
c5dc964f0a
commit
da1aedc694
41 changed files with 1063 additions and 25 deletions
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
|
@ -78,11 +78,11 @@ jobs:
|
|||
run: |
|
||||
npm run test:e2e:edge
|
||||
npm run test:e2e:webdriver
|
||||
# - name: 🐛 Debug Build
|
||||
# uses: stateful/vscode-server-action@v1
|
||||
# if: failure()
|
||||
# with:
|
||||
# timeout: '30000'
|
||||
- name: 🐛 Debug Build
|
||||
uses: stateful/vscode-server-action@v1
|
||||
if: failure()
|
||||
with:
|
||||
timeout: '30000'
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
|
|
|
|||
26
e2e/browser-runner/components/StencilComponent.tsx
Normal file
26
e2e/browser-runner/components/StencilComponent.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Component, Prop, h } from '@stencil/core'
|
||||
import { MatchResults } from '@stencil-community/router'
|
||||
|
||||
@Component({
|
||||
tag: 'app-profile',
|
||||
styleUrl: 'app-profile.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class AppProfile {
|
||||
@Prop() match: MatchResults
|
||||
|
||||
normalize(name: string): string {
|
||||
if (name) {
|
||||
return name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="app-profile">
|
||||
<p>Hello! My name is {this.normalize(this.match?.params.name)}.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
16
e2e/browser-runner/stencil.test.tsx
Normal file
16
e2e/browser-runner/stencil.test.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { $, expect } from '@wdio/globals'
|
||||
import { render } from '@wdio/browser-runner/stencil'
|
||||
|
||||
import { AppProfile } from './components/StencilComponent.jsx'
|
||||
|
||||
describe('Stencil Component Testing', () => {
|
||||
it('should render component correctly', async () => {
|
||||
render({
|
||||
components: [AppProfile],
|
||||
template: () => (
|
||||
<app-profile match={{ params: { name: 'stencil' } }}></app-profile>
|
||||
)
|
||||
})
|
||||
await expect($('>>>.app-profile')).toHaveText('Hello! My name is Stencil.')
|
||||
})
|
||||
})
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
"compilerOptions": {
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["DOM"],
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
|
|
|||
28
e2e/package-lock.json
generated
28
e2e/package-lock.json
generated
|
|
@ -5,6 +5,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@stencil-community/router": "^1.0.2",
|
||||
"lit": "^2.8.0",
|
||||
"preact": "^10.16.0",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -669,6 +670,20 @@
|
|||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@stencil-community/router": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil-community/router/-/router-1.0.2.tgz",
|
||||
"integrity": "sha512-GQkhcwDZdkX6k6rE/ZqrH2mbxGt9wPq0KGAdwPJOcZJbfq9cKaSMOuS7heDEBcWgXgxJS2rKqwUKyKTj41g9pA==",
|
||||
"dependencies": {
|
||||
"@stencil/state-tunnel": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/state-tunnel": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/state-tunnel/-/state-tunnel-1.0.1.tgz",
|
||||
"integrity": "sha512-DYG8uROgL9hkjVTCtCfRBb0d3FwpiFB0muRrNZQ2X1Qo5hxMuNNji76/ILddqeq0AfgkKCW82xrMPDpy+rNIhQ==",
|
||||
"deprecated": "@stencil/state-tunnel is no longer supported"
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
|
||||
|
|
@ -5376,6 +5391,19 @@
|
|||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||
"dev": true
|
||||
},
|
||||
"@stencil-community/router": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil-community/router/-/router-1.0.2.tgz",
|
||||
"integrity": "sha512-GQkhcwDZdkX6k6rE/ZqrH2mbxGt9wPq0KGAdwPJOcZJbfq9cKaSMOuS7heDEBcWgXgxJS2rKqwUKyKTj41g9pA==",
|
||||
"requires": {
|
||||
"@stencil/state-tunnel": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@stencil/state-tunnel": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/state-tunnel/-/state-tunnel-1.0.1.tgz",
|
||||
"integrity": "sha512-DYG8uROgL9hkjVTCtCfRBb0d3FwpiFB0muRrNZQ2X1Qo5hxMuNNji76/ILddqeq0AfgkKCW82xrMPDpy+rNIhQ=="
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
"test:browser:lit": "run-s test:browser:lit:*",
|
||||
"test:browser:lit:wdio": "cross-env WDIO_PRESET=lit node ../packages/wdio-cli/bin/wdio.js ./browser-runner/wdio.conf.js --spec lit.test.js",
|
||||
"test:browser:lit:verifyCoverage": "run-s verifyCoverage",
|
||||
"test:browser:stencil": "run-s test:browser:stencil:*",
|
||||
"test:browser:stencil:wdio": "cross-env WDIO_PRESET=stencil node ../packages/wdio-cli/bin/wdio.js ./browser-runner/wdio.conf.js --spec stencil.test.tsx",
|
||||
"test:browser:stencil:verifyCoverage": "run-s verifyCoverage",
|
||||
"test:mocha": "node ../packages/wdio-cli/bin/wdio.js ./mocha/wdio.conf.js",
|
||||
"test:jasmine": "node ../packages/wdio-cli/bin/wdio.js ./jasmine/wdio.conf.js",
|
||||
"test:cucumber": "node ../packages/wdio-cli/bin/wdio.js ./cucumber/wdio.conf.js",
|
||||
|
|
@ -48,6 +51,7 @@
|
|||
"tailwindcss": "^3.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stencil-community/router": "^1.0.2",
|
||||
"lit": "^2.8.0",
|
||||
"preact": "^10.16.0",
|
||||
"react": "^18.2.0",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { defineConfig } from 'vite'
|
|||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 1000 * 60,
|
||||
testTimeout: 1000 * 60 * 3,
|
||||
include: ['./e2e/**/*.test.ts'],
|
||||
hookTimeout: 60 * 1000,
|
||||
threads: false
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os from 'node:os'
|
||||
import type { Capabilities } from '../../../packages/wdio-types'
|
||||
|
||||
const SCROLL_MARGIN_TRESHOLD = 25
|
||||
|
|
@ -69,6 +70,11 @@ describe('main suite 1', () => {
|
|||
})
|
||||
|
||||
it('should be able to scroll up and down', async () => {
|
||||
if (os.platform() === 'win32') {
|
||||
console.warn('Skipping scroll tests on Windows')
|
||||
return
|
||||
}
|
||||
|
||||
await browser.url('https://webdriver.io/')
|
||||
const currentScrollPosition = await browser.execute(() => [
|
||||
window.scrollX, window.scrollY
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"@cucumber/cucumber": "^9.3.0",
|
||||
"@octokit/rest": "^19.0.13",
|
||||
"@preact/preset-vite": "^2.5.0",
|
||||
"@stencil/core": "^4.0.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.4",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/archiver": "^5.3.2",
|
||||
|
|
@ -3784,6 +3785,19 @@
|
|||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.0.4.tgz",
|
||||
"integrity": "sha512-eE0BcjEH8JjS0XlgWGiQ0c3uxtJdLvnYRv8rK/WokhGdxht31UMhEM8r+pVHbLPD/y6cSApUfxTl1JVcFFs+Xw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.5.tgz",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
"@cucumber/cucumber": "^9.3.0",
|
||||
"@octokit/rest": "^19.0.13",
|
||||
"@preact/preset-vite": "^2.5.0",
|
||||
"@stencil/core": "^4.0.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.4",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/archiver": "^5.3.2",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br />
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br />
|
||||
__Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export type FrameworkPreset = 'react' | 'preact' | 'vue' | 'svelte' | 'lit' | 'solid'
|
||||
export type FrameworkPreset = 'react' | 'preact' | 'vue' | 'svelte' | 'lit' | 'solid' | 'stencil'
|
||||
type Arrayable<T> = T | Array<T>
|
||||
type CoverageReporter = 'clover' | 'cobertura' | 'html-spa' | 'html' | 'json-summary' | 'json' | 'lcov' | 'lcovonly' | 'none' | 'teamcity' | 'text-lcov' | 'text-summary' | 'text'
|
||||
export interface CoverageOptions extends Omit<IstanbulPluginOptions, 'cypress' | 'checkProd' | 'forceBuildInstrument'> {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const PRESET_DEPENDENCIES: Record<FrameworkPreset, [string, string, any]
|
|||
vue: ['@vitejs/plugin-vue', 'default', undefined],
|
||||
svelte: ['@sveltejs/vite-plugin-svelte', 'svelte', undefined],
|
||||
solid: ['vite-plugin-solid', 'default', undefined],
|
||||
stencil: undefined,
|
||||
lit: undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,547 @@
|
|||
import { h } from '@stencil/core'
|
||||
|
||||
process.nextTick = (cb) => setTimeout(cb, 0)
|
||||
|
||||
// @ts-expect-error
|
||||
window.React = {
|
||||
createElement: h,
|
||||
}
|
||||
|
||||
import type {
|
||||
ComponentRuntimeMeta,
|
||||
ComponentTestingConstructor,
|
||||
HostRef,
|
||||
LazyBundlesRuntimeData,
|
||||
NewSpecPageOptions,
|
||||
} from '@stencil/core/internal'
|
||||
import {
|
||||
bootstrapLazy,
|
||||
flushAll,
|
||||
insertVdomAnnotations,
|
||||
registerComponents,
|
||||
registerModule,
|
||||
renderVdom,
|
||||
setSupportsShadowDom,
|
||||
startAutoApplyChanges,
|
||||
styles,
|
||||
win,
|
||||
writeTask,
|
||||
// @ts-expect-error
|
||||
} from '@stencil/core/internal/testing/index.js'
|
||||
|
||||
interface StencilEnvironment {
|
||||
/**
|
||||
* After changes have been made to a component, such as a update to a property or
|
||||
* attribute, the test page does not automatically apply the changes. In order to
|
||||
* wait for, and apply the update, call await `flushAll()`.
|
||||
*/
|
||||
flushAll: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new spec page for unit testing
|
||||
* @param opts the options to apply to the spec page that influence its configuration and operation
|
||||
* @returns the created spec page
|
||||
*/
|
||||
export function render(opts: NewSpecPageOptions): StencilEnvironment {
|
||||
if (!opts) {
|
||||
throw new Error('NewSpecPageOptions required')
|
||||
}
|
||||
|
||||
const stencilStage = document.querySelector('stencil-stage')
|
||||
if (stencilStage) {
|
||||
stencilStage.remove()
|
||||
}
|
||||
const stage = document.createElement('stencil-stage')
|
||||
document.body.appendChild(stage)
|
||||
|
||||
if (Array.isArray(opts.components)) {
|
||||
registerComponents(opts.components)
|
||||
}
|
||||
|
||||
if (opts.hydrateClientSide) {
|
||||
opts.includeAnnotations = true
|
||||
}
|
||||
if (opts.hydrateServerSide) {
|
||||
opts.includeAnnotations = true
|
||||
setSupportsShadowDom(false)
|
||||
} else {
|
||||
opts.includeAnnotations = !!opts.includeAnnotations
|
||||
if (opts.supportsShadowDom === false) {
|
||||
setSupportsShadowDom(false)
|
||||
} else {
|
||||
setSupportsShadowDom(true)
|
||||
}
|
||||
}
|
||||
const cmpTags = new Set<string>()
|
||||
const doc = win.document
|
||||
|
||||
const page = {
|
||||
win: win,
|
||||
doc: doc,
|
||||
body: stage as any,
|
||||
styles: styles as Map<string, string>
|
||||
} as const
|
||||
|
||||
const lazyBundles: LazyBundlesRuntimeData = opts.components.map((Cstr: ComponentTestingConstructor) => {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (Cstr.COMPILER_META == null) {
|
||||
throw new Error('Invalid component class: Missing static "COMPILER_META" property.')
|
||||
}
|
||||
|
||||
cmpTags.add(Cstr.COMPILER_META.tagName)
|
||||
Cstr.isProxied = false
|
||||
|
||||
proxyComponentLifeCycles(Cstr)
|
||||
|
||||
const bundleId = `${Cstr.COMPILER_META.tagName}.${Math.round(Math.random() * 899999) + 100000}`
|
||||
const stylesMeta = Cstr.COMPILER_META.styles
|
||||
if (Array.isArray(stylesMeta)) {
|
||||
if (stylesMeta.length > 1) {
|
||||
const styles: any = {}
|
||||
stylesMeta.forEach((style) => {
|
||||
styles[style.modeName] = style.styleStr
|
||||
})
|
||||
Cstr.style = styles
|
||||
} else if (stylesMeta.length === 1) {
|
||||
Cstr.style = stylesMeta[0].styleStr
|
||||
}
|
||||
}
|
||||
registerModule(bundleId, Cstr)
|
||||
|
||||
const lazyBundleRuntimeMeta = formatLazyBundleRuntimeMeta(bundleId, [Cstr.COMPILER_META])
|
||||
return lazyBundleRuntimeMeta
|
||||
})
|
||||
|
||||
if (typeof opts.direction === 'string') {
|
||||
page.doc.documentElement.setAttribute('dir', opts.direction)
|
||||
}
|
||||
|
||||
if (typeof opts.language === 'string') {
|
||||
page.doc.documentElement.setAttribute('lang', opts.language)
|
||||
}
|
||||
|
||||
bootstrapLazy(lazyBundles)
|
||||
|
||||
if (typeof opts.template === 'function') {
|
||||
const cmpMeta: ComponentRuntimeMeta = {
|
||||
$flags$: 0,
|
||||
$tagName$: 'body',
|
||||
}
|
||||
const ref: HostRef = {
|
||||
$ancestorComponent$: undefined,
|
||||
$flags$: 0,
|
||||
$modeName$: undefined,
|
||||
$cmpMeta$: cmpMeta,
|
||||
$hostElement$: page.body,
|
||||
}
|
||||
renderVdom(ref, opts.template())
|
||||
}
|
||||
|
||||
let rootComponent: any = null
|
||||
Object.defineProperty(page, 'root', {
|
||||
get() {
|
||||
if (!rootComponent) {
|
||||
rootComponent = findRootComponent(cmpTags, page.body)
|
||||
}
|
||||
if (rootComponent) {
|
||||
return rootComponent
|
||||
}
|
||||
const firstElementChild = page.body.firstElementChild
|
||||
if (!firstElementChild) {
|
||||
return firstElementChild as any
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
|
||||
if (opts.hydrateServerSide) {
|
||||
insertVdomAnnotations(doc, [])
|
||||
}
|
||||
|
||||
if (opts.autoApplyChanges) {
|
||||
startAutoApplyChanges()
|
||||
}
|
||||
|
||||
flushAll()
|
||||
return { flushAll }
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method that proxies Stencil lifecycle methods by mutating the provided component class
|
||||
* @param Cstr the component class whose lifecycle methods will be proxied
|
||||
*/
|
||||
function proxyComponentLifeCycles(Cstr: ComponentTestingConstructor): void {
|
||||
// we may have called this function on the class before, reset the state of the class
|
||||
if (typeof Cstr.prototype?.__componentWillLoad === 'function') {
|
||||
Cstr.prototype.componentWillLoad = Cstr.prototype.__componentWillLoad
|
||||
Cstr.prototype.__componentWillLoad = null
|
||||
}
|
||||
if (typeof Cstr.prototype?.__componentWillUpdate === 'function') {
|
||||
Cstr.prototype.componentWillUpdate = Cstr.prototype.__componentWillUpdate
|
||||
Cstr.prototype.__componentWillUpdate = null
|
||||
}
|
||||
if (typeof Cstr.prototype?.__componentWillRender === 'function') {
|
||||
Cstr.prototype.componentWillRender = Cstr.prototype.__componentWillRender
|
||||
Cstr.prototype.__componentWillRender = null
|
||||
}
|
||||
|
||||
// the class should be in a known 'good' state to proxy functions
|
||||
if (typeof Cstr.prototype?.componentWillLoad === 'function') {
|
||||
Cstr.prototype.__componentWillLoad = Cstr.prototype.componentWillLoad
|
||||
Cstr.prototype.componentWillLoad = function () {
|
||||
// @ts-expect-error
|
||||
const result = this.__componentWillLoad()
|
||||
if (result && typeof result.then === 'function') {
|
||||
writeTask(() => result)
|
||||
} else {
|
||||
writeTask(() => Promise.resolve())
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Cstr.prototype?.componentWillUpdate === 'function') {
|
||||
Cstr.prototype.__componentWillUpdate = Cstr.prototype.componentWillUpdate
|
||||
Cstr.prototype.componentWillUpdate = function () {
|
||||
// @ts-expect-error
|
||||
const result = this.__componentWillUpdate()
|
||||
if (result && typeof result.then === 'function') {
|
||||
writeTask(() => result)
|
||||
} else {
|
||||
writeTask(() => Promise.resolve())
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Cstr.prototype?.componentWillRender === 'function') {
|
||||
Cstr.prototype.__componentWillRender = Cstr.prototype.componentWillRender
|
||||
Cstr.prototype.componentWillRender = function () {
|
||||
// @ts-expect-error
|
||||
const result = this.__componentWillRender()
|
||||
if (result && typeof result.then === 'function') {
|
||||
writeTask(() => result)
|
||||
} else {
|
||||
writeTask(() => Promise.resolve())
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first Element whose {@link Element#nodeName} property matches a tag found in the provided `cmpTags`
|
||||
* argument.
|
||||
*
|
||||
* If the `nodeName` property on the element matches any of the names found in the provided `cmpTags` argument, that
|
||||
* element is returned. If no match is found on the current element, the children will be inspected in a depth-first
|
||||
* search manner. This process continues until either:
|
||||
* - an element is found (and execution ends)
|
||||
* - no element is found after an exhaustive search
|
||||
*
|
||||
* @param cmpTags component tag names to use in the match criteria
|
||||
* @param node the node whose children are to be inspected
|
||||
* @returns An element whose name matches one of the strings in the provided `cmpTags`. If no match is found, `null` is
|
||||
* returned
|
||||
*/
|
||||
function findRootComponent(cmpTags: Set<string>, node: Element): Element | null {
|
||||
if (node) {
|
||||
const children = node.children
|
||||
const childrenLength = children.length
|
||||
|
||||
for (let i = 0; i < childrenLength; i++) {
|
||||
const elm = children[i]
|
||||
if (cmpTags.has(elm.nodeName.toLowerCase())) {
|
||||
return elm
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < childrenLength; i++) {
|
||||
const r = findRootComponent(cmpTags, children[i])
|
||||
if (r) {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of flags used for bitwise calculations against {@link ComponentRuntimeMeta#$flags$}.
|
||||
*
|
||||
* These flags should only be used in conjunction with {@link ComponentRuntimeMeta#$flags$}.
|
||||
* They should _not_ be used for calculations against other fields/numbers
|
||||
*/
|
||||
export const enum CMP_FLAGS {
|
||||
/**
|
||||
* Used to determine if a component is using the shadow DOM.
|
||||
* e.g. `shadow: true | {}` is set on the `@Component()` decorator
|
||||
*/
|
||||
shadowDomEncapsulation = 1 << 0,
|
||||
/**
|
||||
* Used to determine if a component is using scoped stylesheets
|
||||
* e.g. `scoped: true` is set on the `@Component()` decorator
|
||||
*/
|
||||
scopedCssEncapsulation = 1 << 1,
|
||||
/**
|
||||
* Used to determine if a component does not use the shadow DOM _and_ has `<slot/>` tags in its markup.
|
||||
*/
|
||||
hasSlotRelocation = 1 << 2,
|
||||
// TODO(STENCIL-854): Remove code related to legacy shadowDomShim field
|
||||
// Note that when we remove this field we should consider whether we need to
|
||||
// retain a placeholder here, since if we want to have compatability between
|
||||
// different versions of the runtime then we'll need to not shift the values
|
||||
// of the other higher flags down
|
||||
/**
|
||||
* Determines if a shim for the shadow DOM is necessary.
|
||||
*
|
||||
* The shim should only be needed if a component requires {@link shadowDomEncapsulation} and if any output
|
||||
* target-specific criteria are met. Refer to this flag's usage to determine each output target's criteria.
|
||||
*/
|
||||
needsShadowDomShim = 1 << 3,
|
||||
/**
|
||||
* Determines if `delegatesFocus` is enabled for a component that uses the shadow DOM.
|
||||
* e.g. `shadow: { delegatesFocus: true }` is set on the `@Component()` decorator
|
||||
*/
|
||||
shadowDelegatesFocus = 1 << 4,
|
||||
/**
|
||||
* Determines if `mode` is set on the `@Component()` decorator
|
||||
*/
|
||||
hasMode = 1 << 5,
|
||||
// TODO(STENCIL-854): Remove code related to legacy shadowDomShim field
|
||||
/**
|
||||
* Determines if styles must be scoped due to either:
|
||||
* 1. A component is using scoped stylesheets ({@link scopedCssEncapsulation})
|
||||
* 2. A component is using the shadow DOM _and_ the output target's rules for requiring a shadow DOM shim have been
|
||||
* met ({@link needsShadowDomShim})
|
||||
*/
|
||||
needsScopedEncapsulation = scopedCssEncapsulation | needsShadowDomShim,
|
||||
}
|
||||
|
||||
const formatLazyBundleRuntimeMeta = (
|
||||
bundleId: any,
|
||||
cmps: any[],
|
||||
): any => {
|
||||
return [bundleId, cmps.map((cmp) => formatComponentRuntimeMeta(cmp, true))]
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform metadata about a component from the compiler to a compact form for
|
||||
* use at runtime.
|
||||
*
|
||||
* @param compilerMeta component metadata gathered during compilation
|
||||
* @param includeMethods include methods in the component's members or not
|
||||
* @returns a compact format for component metadata, intended for runtime use
|
||||
*/
|
||||
export const formatComponentRuntimeMeta = (
|
||||
compilerMeta: any,
|
||||
includeMethods: boolean,
|
||||
): any => {
|
||||
let flags = 0
|
||||
if (compilerMeta.encapsulation === 'shadow') {
|
||||
flags |= CMP_FLAGS.shadowDomEncapsulation
|
||||
if (compilerMeta.shadowDelegatesFocus) {
|
||||
flags |= CMP_FLAGS.shadowDelegatesFocus
|
||||
}
|
||||
} else if (compilerMeta.encapsulation === 'scoped') {
|
||||
flags |= CMP_FLAGS.scopedCssEncapsulation
|
||||
}
|
||||
if (compilerMeta.encapsulation !== 'shadow' && compilerMeta.htmlTagNames.includes('slot')) {
|
||||
flags |= CMP_FLAGS.hasSlotRelocation
|
||||
}
|
||||
if (compilerMeta.hasMode) {
|
||||
flags |= CMP_FLAGS.hasMode
|
||||
}
|
||||
|
||||
const members = formatComponentRuntimeMembers(compilerMeta, includeMethods)
|
||||
const hostListeners = formatHostListeners(compilerMeta)
|
||||
return trimFalsy([
|
||||
flags,
|
||||
compilerMeta.tagName,
|
||||
Object.keys(members).length > 0 ? members : undefined,
|
||||
hostListeners.length > 0 ? hostListeners : undefined,
|
||||
])
|
||||
}
|
||||
|
||||
const formatComponentRuntimeMembers = (
|
||||
compilerMeta: any,
|
||||
includeMethods = true,
|
||||
): any => {
|
||||
return {
|
||||
...formatPropertiesRuntimeMember(compilerMeta.properties),
|
||||
...formatStatesRuntimeMember(compilerMeta.states),
|
||||
...(includeMethods ? formatMethodsRuntimeMember(compilerMeta.methods) : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const formatPropertiesRuntimeMember = (properties: any[]) => {
|
||||
const runtimeMembers: any = {}
|
||||
|
||||
properties.forEach((member) => {
|
||||
runtimeMembers[member.name] = trimFalsy([
|
||||
/**
|
||||
* [0] member type
|
||||
*/
|
||||
formatFlags(member),
|
||||
formatAttrName(member),
|
||||
])
|
||||
})
|
||||
return runtimeMembers
|
||||
}
|
||||
|
||||
const formatFlags = (compilerProperty: any) => {
|
||||
let type = formatPropType(compilerProperty.type)
|
||||
if (compilerProperty.mutable) {
|
||||
type |= MEMBER_FLAGS.Mutable
|
||||
}
|
||||
if (compilerProperty.reflect) {
|
||||
type |= MEMBER_FLAGS.ReflectAttr
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
const formatAttrName = (compilerProperty: any) => {
|
||||
if (typeof compilerProperty.attribute === 'string') {
|
||||
// string attr name means we should observe this attribute
|
||||
if (compilerProperty.name === compilerProperty.attribute) {
|
||||
// property name and attribute name are the exact same
|
||||
// true value means to use the property name for the attribute name
|
||||
return undefined
|
||||
}
|
||||
|
||||
// property name and attribute name are not the same
|
||||
// so we need to return the actual string value
|
||||
// example: "multiWord" !== "multi-word"
|
||||
return compilerProperty.attribute
|
||||
}
|
||||
|
||||
// we shouldn't even observe an attribute for this property
|
||||
return undefined
|
||||
}
|
||||
const enum MEMBER_FLAGS {
|
||||
String = 1 << 0,
|
||||
Number = 1 << 1,
|
||||
Boolean = 1 << 2,
|
||||
Any = 1 << 3,
|
||||
Unknown = 1 << 4,
|
||||
|
||||
State = 1 << 5,
|
||||
Method = 1 << 6,
|
||||
Event = 1 << 7,
|
||||
Element = 1 << 8,
|
||||
|
||||
ReflectAttr = 1 << 9,
|
||||
Mutable = 1 << 10,
|
||||
|
||||
Prop = String | Number | Boolean | Any | Unknown,
|
||||
HasAttribute = String | Number | Boolean | Any,
|
||||
PropLike = Prop | State,
|
||||
}
|
||||
|
||||
const formatPropType = (type: any) => {
|
||||
if (type === 'string') {
|
||||
return MEMBER_FLAGS.String
|
||||
}
|
||||
if (type === 'number') {
|
||||
return MEMBER_FLAGS.Number
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return MEMBER_FLAGS.Boolean
|
||||
}
|
||||
if (type === 'any') {
|
||||
return MEMBER_FLAGS.Any
|
||||
}
|
||||
return MEMBER_FLAGS.Unknown
|
||||
}
|
||||
|
||||
const formatStatesRuntimeMember = (states: any[]) => {
|
||||
const runtimeMembers: any = {}
|
||||
|
||||
states.forEach((member) => {
|
||||
runtimeMembers[member.name] = [
|
||||
/**
|
||||
* [0] member flags
|
||||
*/
|
||||
MEMBER_FLAGS.State,
|
||||
]
|
||||
})
|
||||
return runtimeMembers
|
||||
}
|
||||
|
||||
const formatMethodsRuntimeMember = (methods: any[]) => {
|
||||
const runtimeMembers: any = {}
|
||||
|
||||
methods.forEach((member) => {
|
||||
runtimeMembers[member.name] = [
|
||||
/**
|
||||
* [0] member flags
|
||||
*/
|
||||
MEMBER_FLAGS.Method,
|
||||
]
|
||||
})
|
||||
return runtimeMembers
|
||||
}
|
||||
|
||||
const formatHostListeners = (compilerMeta: any) => {
|
||||
return compilerMeta.listeners.map((compilerListener: any) => {
|
||||
const hostListener: any = [
|
||||
computeListenerFlags(compilerListener),
|
||||
compilerListener.name,
|
||||
compilerListener.method,
|
||||
]
|
||||
return hostListener
|
||||
})
|
||||
}
|
||||
|
||||
const enum LISTENER_FLAGS {
|
||||
Passive = 1 << 0,
|
||||
Capture = 1 << 1,
|
||||
|
||||
TargetDocument = 1 << 2,
|
||||
TargetWindow = 1 << 3,
|
||||
TargetBody = 1 << 4,
|
||||
|
||||
/**
|
||||
* @deprecated Prevented from new apps, but left in for older collections
|
||||
*/
|
||||
TargetParent = 1 << 5,
|
||||
}
|
||||
|
||||
const computeListenerFlags = (listener: any) => {
|
||||
let flags = 0
|
||||
if (listener.capture) {
|
||||
flags |= LISTENER_FLAGS.Capture
|
||||
}
|
||||
if (listener.passive) {
|
||||
flags |= LISTENER_FLAGS.Passive
|
||||
}
|
||||
switch (listener.target) {
|
||||
case 'document':
|
||||
flags |= LISTENER_FLAGS.TargetDocument
|
||||
break
|
||||
case 'window':
|
||||
flags |= LISTENER_FLAGS.TargetWindow
|
||||
break
|
||||
case 'body':
|
||||
flags |= LISTENER_FLAGS.TargetBody
|
||||
break
|
||||
case 'parent' as any:
|
||||
flags |= LISTENER_FLAGS.TargetParent
|
||||
break
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
const trimFalsy = (data: any): any => {
|
||||
const arr = data as any[]
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (arr[i]) {
|
||||
break
|
||||
}
|
||||
// if falsy, safe to pop()
|
||||
arr.pop()
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import type { InlineConfig } from 'vite'
|
|||
|
||||
import { isNuxtFramework, optimizeForNuxt } from './nuxt.js'
|
||||
import { isUsingTailwindCSS, optimizeForTailwindCSS } from './tailwindcss.js'
|
||||
import { isUsingStencilJS, optimizeForStencil } from './stencil.js'
|
||||
|
||||
export default async function updateViteConfig (options: WebdriverIO.BrowserRunnerOptions, config: Options.Testrunner) {
|
||||
const optimizations: InlineConfig = {}
|
||||
|
|
@ -18,5 +19,9 @@ export default async function updateViteConfig (options: WebdriverIO.BrowserRunn
|
|||
Object.assign(optimizations, await optimizeForTailwindCSS(rootDir))
|
||||
}
|
||||
|
||||
if (isUsingStencilJS(rootDir, options)) {
|
||||
Object.assign(optimizations, await optimizeForStencil(rootDir))
|
||||
}
|
||||
|
||||
return optimizations
|
||||
}
|
||||
|
|
|
|||
76
packages/wdio-browser-runner/src/vite/frameworks/stencil.ts
Normal file
76
packages/wdio-browser-runner/src/vite/frameworks/stencil.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
|
||||
import type { InlineConfig, Plugin } from 'vite'
|
||||
|
||||
import { hasFileByExtensions } from '../utils.js'
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
export function isUsingStencilJS (rootDir: string, options: WebdriverIO.BrowserRunnerOptions) {
|
||||
return Boolean(options.preset === 'stencil' || hasFileByExtensions(path.join(rootDir, 'stencil.config')))
|
||||
}
|
||||
|
||||
export async function optimizeForStencil (rootDir: string) {
|
||||
const stencilConfig = await import(path.join(rootDir, 'stencil.config.ts')).catch(() => ({ config: {} }))
|
||||
const stencilPlugins = stencilConfig.config.plugins
|
||||
const stencilOptimizations: InlineConfig = {
|
||||
plugins: [await stencilVitePlugin(rootDir)]
|
||||
}
|
||||
|
||||
if (stencilPlugins) {
|
||||
const esbuildPlugin = stencilPlugins.find((plugin: any) => plugin.name === 'esbuild-plugin')
|
||||
if (esbuildPlugin) {
|
||||
stencilOptimizations.optimizeDeps = {
|
||||
include: esbuildPlugin.options.include
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stencilOptimizations
|
||||
}
|
||||
|
||||
async function stencilVitePlugin (rootDir: string): Promise<Plugin> {
|
||||
const { transpileSync } = await import('@stencil/core/compiler/stencil.js')
|
||||
const stencilHelperPath = path.resolve(__dirname, 'fixtures', 'stencil.js')
|
||||
return {
|
||||
name: 'wdio-stencil',
|
||||
enforce: 'pre',
|
||||
resolveId (source) {
|
||||
if (source === '@wdio/browser-runner/stencil') {
|
||||
return stencilHelperPath
|
||||
}
|
||||
},
|
||||
transform: function (code, id) {
|
||||
if (!id.includes('StencilComponent.tsx')) {
|
||||
return { code }
|
||||
}
|
||||
|
||||
const opts = {
|
||||
componentExport: 'customelement',
|
||||
componentMetadata: 'compilerstatic',
|
||||
coreImportPath: '@stencil/core/internal/testing',
|
||||
currentDirectory: rootDir,
|
||||
module: 'esm',
|
||||
proxy: null,
|
||||
sourceMap: 'inline',
|
||||
style: null,
|
||||
styleImportData: 'queryparams',
|
||||
target: 'es2018',
|
||||
transformAliasedImportPaths: process.env.__STENCIL_TRANSPILE_PATHS__ === 'true',
|
||||
}
|
||||
|
||||
const transpiledCode = transpileSync(code, opts)
|
||||
return {
|
||||
...transpiledCode,
|
||||
code: transpiledCode.code
|
||||
// HTMLElement gets imported from StencilJS, but we need to use the one from the browser
|
||||
.replace('extends HTMLElement {', 'extends window.HTMLElement {')
|
||||
// make sure that components are exported properly
|
||||
// StencilJS removes the export when componentExport is set to 'customelement'
|
||||
.replace('\nconst', '\nexport const'),
|
||||
inputFilePath: id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,3 +2,4 @@ declare module 'unimport/unplugin'
|
|||
declare module 'unimport'
|
||||
declare module 'tailwindcss'
|
||||
declare module '@nuxt/kit'
|
||||
declare module '@stencil/core/compiler/stencil.js'
|
||||
|
|
|
|||
70
packages/wdio-browser-runner/stencil/index.d.ts
vendored
Normal file
70
packages/wdio-browser-runner/stencil/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
|
||||
/**
|
||||
* Options pertaining to the creation and functionality of a {@link SpecPage}
|
||||
*/
|
||||
interface RenderOptions {
|
||||
/**
|
||||
* An array of components to test. Component classes can be imported into the spec file, then their reference should be added to the `component` array in order to be used throughout the test.
|
||||
*/
|
||||
components: any[];
|
||||
/**
|
||||
* If `false`, do not flush the render queue on initial test setup.
|
||||
*/
|
||||
flushQueue?: boolean;
|
||||
/**
|
||||
* The initial JSX used to generate the test.
|
||||
* Use `template` when you want to initialize a component using their properties, instead of their HTML attributes.
|
||||
* It will render the specified template (JSX) into `document.body`.
|
||||
*/
|
||||
template?: () => any;
|
||||
/**
|
||||
* Sets the mocked `lang` attribute on `<html>`.
|
||||
*/
|
||||
language?: string;
|
||||
/**
|
||||
* Useful for debugging hydrating components client-side. Sets that the `html` option already includes annotated prerender attributes and comments.
|
||||
*/
|
||||
hydrateClientSide?: boolean;
|
||||
/**
|
||||
* Useful for debugging hydrating components server-side. The output HTML will also include prerender annotations.
|
||||
*/
|
||||
hydrateServerSide?: boolean;
|
||||
/**
|
||||
* Sets the mocked `document.referrer`.
|
||||
*/
|
||||
referrer?: string;
|
||||
/**
|
||||
* Manually set if the mocked document supports Shadow DOM or not. Default is `true`.
|
||||
*/
|
||||
supportsShadowDom?: boolean;
|
||||
/**
|
||||
* When a component is pre-rendered it includes HTML annotations, such as `s-id` attributes and `<!-t.0->` comments. This information is used by clientside hydrating. Default is `false`.
|
||||
*/
|
||||
includeAnnotations?: boolean;
|
||||
/**
|
||||
* By default, any changes to component properties and attributes must `page.waitForChanges()` in order to test the updates. As an option, `autoAppluChanges` continuously flushes the queue on the background. Default is `false`.
|
||||
*/
|
||||
autoApplyChanges?: boolean;
|
||||
/**
|
||||
* By default, styles are not attached to the DOM and they are not reflected in the serialized HTML.
|
||||
* Setting this option to `true` will include the component's styles in the serializable output.
|
||||
*/
|
||||
attachStyles?: boolean;
|
||||
/**
|
||||
* Set {@link BuildConditionals} for testing based off the metadata of the component under test.
|
||||
* When `true` all `BuildConditionals` will be assigned to the global testing `BUILD` object, regardless of their
|
||||
* value. When `false`, only `BuildConditionals` with a value of `true` will be assigned to the `BUILD` object.
|
||||
*/
|
||||
strictBuild?: boolean;
|
||||
}
|
||||
|
||||
interface StencilEnvironment {
|
||||
/**
|
||||
* After changes have been made to a component, such as a update to a property or
|
||||
* attribute, the test page does not automatically apply the changes. In order to
|
||||
* wait for, and apply the update, call await `flushAll()`.
|
||||
*/
|
||||
flushAll: () => void
|
||||
}
|
||||
|
||||
export function render(opts: RenderOptions): StencilEnvironment
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import type { Plugin } from 'vite'
|
||||
|
||||
import { expect, vi, test } from 'vitest'
|
||||
import { isUsingStencilJS, optimizeForStencil } from '../../../src/vite/frameworks/stencil.js'
|
||||
import { hasFileByExtensions } from '../../../src/vite/utils.js'
|
||||
|
||||
vi.mock('@stencil/core/compiler/stencil.js', () => ({
|
||||
transpileSync: vi.fn().mockReturnValue({
|
||||
code: 'the transpiled code',
|
||||
inputFilePath: '/foo/bar/StencilComponent.tsx'
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../../../src/vite/utils.js', () => ({
|
||||
hasFileByExtensions: vi.fn(),
|
||||
hasDir: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('/foo/bar/stencil.config.ts', () => ({
|
||||
config: {
|
||||
plugins: [{
|
||||
name: 'esbuild-plugin',
|
||||
options: {
|
||||
include: ['foo', 'bar']
|
||||
}
|
||||
}]
|
||||
}
|
||||
}))
|
||||
|
||||
test('isNuxtFramework', async () => {
|
||||
expect(await isUsingStencilJS('/foo/bar', {})).toBe(false)
|
||||
expect(await isUsingStencilJS('/foo/bar', { preset: 'stencil' })).toBe(true)
|
||||
vi.mocked(hasFileByExtensions).mockResolvedValueOnce('true')
|
||||
expect(await isUsingStencilJS('/foo/bar', {})).toBe(true)
|
||||
})
|
||||
|
||||
test('optimizeForStencil', async () => {
|
||||
const opt = await optimizeForStencil('/foo/bar')
|
||||
expect(opt).toEqual({
|
||||
optimizeDeps: {
|
||||
include: ['foo', 'bar']
|
||||
},
|
||||
plugins: [expect.any(Object)]
|
||||
})
|
||||
expect((opt.plugins?.[0] as Plugin).name).toBe('wdio-stencil')
|
||||
expect((opt.plugins?.[0] as Plugin).enforce).toBe('pre')
|
||||
expect((opt.plugins?.[0] as any).resolveId('foo')).toBe(undefined)
|
||||
expect(typeof (opt.plugins?.[0] as any).resolveId('@wdio/browser-runner/stencil')).toBe('string')
|
||||
expect((opt.plugins?.[0] as any).transform('foo', '/foo/bar/StencilComponent.tsx', {}))
|
||||
.toEqual({
|
||||
code: 'the transpiled code',
|
||||
inputFilePath: '/foo/bar/StencilComponent.tsx'
|
||||
})
|
||||
})
|
||||
|
|
@ -149,6 +149,7 @@ export const SUPPORTED_BROWSER_RUNNER_PRESETS = [
|
|||
{ name: 'Vue.js (https://vuejs.org/)', value: '@vitejs/plugin-vue$--$vue' },
|
||||
{ name: 'Svelte (https://svelte.dev/)', value: '@sveltejs/vite-plugin-svelte$--$svelte' },
|
||||
{ name: 'SolidJS (https://www.solidjs.com/)', value: 'vite-plugin-solid$--$solid' },
|
||||
{ name: 'StencilJS (https://stenciljs.com/)', value: '$--$stencil' },
|
||||
{ name: 'React (https://reactjs.org/)', value: '@vitejs/plugin-react$--$react' },
|
||||
{ name: 'Preact (https://preactjs.com/)', value: '@preact/preset-vite$--$preact' },
|
||||
{ name: 'Other', value: false }
|
||||
|
|
@ -438,7 +439,15 @@ export const QUESTIONNAIRE = [{
|
|||
type: 'list',
|
||||
name: 'isUsingCompiler',
|
||||
message: 'Do you want to use a compiler?',
|
||||
choices: Object.values(CompilerOptions),
|
||||
choices: (answers: Questionnair) => {
|
||||
/**
|
||||
* StencilJS only supports TypeScript
|
||||
*/
|
||||
if (answers.preset && answers.preset.includes('stencil')) {
|
||||
return [CompilerOptions.TS]
|
||||
}
|
||||
return Object.values(CompilerOptions)
|
||||
},
|
||||
default: /* istanbul ignore next */ (answers: Questionnair) => detectCompiler(answers)
|
||||
}, {
|
||||
type: 'confirm',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { Component, Prop, h, Fragment } from '@stencil/core'
|
||||
|
||||
@Component({
|
||||
tag: 'my-element',
|
||||
shadow: true,
|
||||
styleUrl: 'Component.css'
|
||||
})
|
||||
export class MyElement {
|
||||
@Prop() count = 0
|
||||
@Prop() docsHint = 'Click on the Vite and Lit logos to learn more'
|
||||
|
||||
normalize(name: string): string {
|
||||
if (name) {
|
||||
return name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://webdriver.io/docs/component-testing" target="_blank">
|
||||
<img src="https://webdriver.io/assets/images/robot-3677788dd63849c56aa5cb3f332b12d5.svg" class="logo"
|
||||
alt="WebdriverIO logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>
|
||||
<slot />
|
||||
</h1>
|
||||
<div class="card">
|
||||
<button onClick={() => { this.count++ }} part="button">
|
||||
count is {this.count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/Component.test.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p class="read-the-docs">{this.docsHint}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { render } from '@wdio/browser-runner/stencil'
|
||||
import { $, expect } from '@wdio/globals'
|
||||
|
||||
import { MyElement } from './Component.js'
|
||||
|
||||
describe('Lit component testing', () => {
|
||||
it('should increment value on click', async () => {
|
||||
await render({
|
||||
components: [MyElement],
|
||||
template: () => (
|
||||
<my-element count={42}>WebdriverIO Component Testing</my-element>
|
||||
)
|
||||
})
|
||||
|
||||
const button = await $('my-element').$('>>>button')
|
||||
await expect(button).toHaveText('count is 42')
|
||||
|
||||
await button.click()
|
||||
await button.click()
|
||||
|
||||
await expect(button).toHaveText('count is 44')
|
||||
})
|
||||
})
|
||||
|
|
@ -352,7 +352,7 @@ export async function generateTestFiles(answers: ParsedAnswers) {
|
|||
return generateBrowserRunnerTestFiles(answers)
|
||||
}
|
||||
|
||||
const TSX_BASED_FRAMEWORKS = ['react', 'preact', 'solid']
|
||||
const TSX_BASED_FRAMEWORKS = ['react', 'preact', 'solid', 'stencil']
|
||||
export async function generateBrowserRunnerTestFiles(answers: ParsedAnswers) {
|
||||
const isUsingFramework = typeof answers.preset === 'string'
|
||||
const preset = getPreset(answers)
|
||||
|
|
@ -404,7 +404,7 @@ async function generateLocalRunnerTestFiles(answers: ParsedAnswers) {
|
|||
|
||||
for (const file of files) {
|
||||
const renderedTpl = await renderFile(file, { answers })
|
||||
const isJSX = answers.preset && ['preact', 'react'].includes(answers.preset)
|
||||
const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset)
|
||||
const fileEnding = (answers.isUsingTypeScript ? '.ts' : '.js') + (isJSX ? 'x' : '')
|
||||
const destPath = (
|
||||
file.endsWith('page.js.ejs')
|
||||
|
|
@ -672,6 +672,13 @@ export function npmInstall(parsedAnswers: ParsedAnswers, useYarn: boolean, npmTa
|
|||
parsedAnswers.packagesToInstall.push('lit')
|
||||
}
|
||||
|
||||
/**
|
||||
* add dependency for Stencil testing
|
||||
*/
|
||||
if (preset === 'stencil') {
|
||||
parsedAnswers.packagesToInstall.push('@stencil/core')
|
||||
}
|
||||
|
||||
/**
|
||||
* add helper for React rendering when not using Testing Library
|
||||
*/
|
||||
|
|
@ -798,6 +805,14 @@ export async function setupTypeScript(parsedAnswers: ParsedAnswers) {
|
|||
jsx: 'preserve',
|
||||
jsxImportSource: 'solid-js'
|
||||
}
|
||||
: {},
|
||||
preset === 'stencil'
|
||||
? {
|
||||
experimentalDecorators: true,
|
||||
jsx: 'react',
|
||||
jsxFactory: 'h',
|
||||
jsxFragmentFactory: 'Fragment'
|
||||
}
|
||||
: {}
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ EventEmitter.defaultMaxListeners = packages.length + 3
|
|||
const ROOT_DIR = path.join(__dirname, '..')
|
||||
|
||||
const IGNORE_PACKAGES: IgnoredPackages = {
|
||||
'wdio-browser-runner': ['virtual:wdio', 'mocha', '@nuxt/kit', 'unimport', 'unimport/unplugin']
|
||||
'wdio-browser-runner': ['virtual:wdio', 'mocha', '@nuxt/kit', 'unimport', 'unimport/unplugin', '@stencil/core']
|
||||
}
|
||||
|
||||
const brokenPackages = (await Promise.all(packages.map(async (pkg) => {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@
|
|||
"component-testing/svelte",
|
||||
"component-testing/solid",
|
||||
"component-testing/react",
|
||||
"component-testing/preact"
|
||||
"component-testing/preact",
|
||||
"component-testing/stencil"
|
||||
]
|
||||
},
|
||||
"component-testing/coverage",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ This runner uses [Vite](https://vitejs.dev/) to compile your test code and load
|
|||
- Vue.js
|
||||
- Svelte
|
||||
- SolidJS
|
||||
- Stencil
|
||||
|
||||
Every test file / test file group runs within a single page which means that between each test the page is being reloaded to guarantee isolation between tests.
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ import { render } from 'solid-js/web'
|
|||
|
||||
import App from './components/Component.jsx'
|
||||
|
||||
describe('React Component Testing', () => {
|
||||
describe('Solid Component Testing', () => {
|
||||
/**
|
||||
* ensure we render the component for every test in a
|
||||
* new root container
|
||||
|
|
|
|||
88
website/docs/component-testing/Stencil.md
Normal file
88
website/docs/component-testing/Stencil.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
id: stencil
|
||||
title: Stencil
|
||||
---
|
||||
|
||||
[Stencil](https://stenciljs.com/) is a library for building reusable, scalable component libraries. You can test Stencil components directly in a real browser using WebdriverIO and its [browser runner](/docs/runner#browser-runner).
|
||||
|
||||
## Setup
|
||||
|
||||
To setup WebdriverIO within your Stencil project, follow the [instructions](/docs/component-testing#set-up) in our component testing docs. Make sure to select `stencil` as preset within your runner options, e.g.:
|
||||
|
||||
```js
|
||||
// wdio.conf.js
|
||||
export const config = {
|
||||
// ...
|
||||
runner: ['browser', {
|
||||
preset: 'stencil'
|
||||
}],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
In case you use Stencil with a framework like React or Vue, you should keep the preset for these frameworks.
|
||||
|
||||
:::
|
||||
|
||||
You can then start the tests by running:
|
||||
|
||||
```sh
|
||||
npx wdio run ./wdio.conf.ts
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Given you have the following Stencil component:
|
||||
|
||||
```tsx title="./components/Component.tsx"
|
||||
import { Component, Prop, h } from '@stencil/core'
|
||||
|
||||
@Component({
|
||||
tag: 'my-name',
|
||||
shadow: true
|
||||
})
|
||||
export class MyName {
|
||||
@Prop() name: string
|
||||
|
||||
normalize(name: string): string {
|
||||
if (name) {
|
||||
return name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="text">
|
||||
<p>Hello! My name is {this.normalize(this.name)}.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In your test use the `render` method from `@wdio/browser-runner/stencil` to attach the component to the test page. To interact with the component we recommend to use WebdriverIO commands as they behave more close to actual user interactions, e.g.:
|
||||
|
||||
```tsx title="app.test.tsx"
|
||||
import { expect } from '@wdio/globals'
|
||||
import { render } from '@wdio/browser-runner/stencil'
|
||||
|
||||
import MyNameComponent from './components/Component.tsx'
|
||||
|
||||
describe('Stencil Component Testing', () => {
|
||||
it('should render component correctly', async () => {
|
||||
await render({
|
||||
components: [MyNameComponent],
|
||||
template: () => (
|
||||
<my-name name={'stencil'}></my-name>
|
||||
)
|
||||
})
|
||||
await expect($('>>>.text')).toHaveText('Hello! My name is Stencil.')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
You can find a full example of a WebdriverIO component test suite for SolidJS in our [example repository](https://github.com/webdriverio/component-testing-examples).
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ Der Browser-Runner erlaubt folgende Konfigurationen:
|
|||
|
||||
Wenn Sie Komponenten mit einem der oben genannten Frameworks testen, können Sie eine Voreinstellung definieren, die sicherstellt, dass alles sofort konfiguriert ist.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ El corredor del navegador permite las siguientes configuraciones:
|
|||
|
||||
Si prueba componentes usando uno de los frameworks mencionados arriba, puede definir un preset que asegura que todo está configurado de nuevo. Esta opción no se puede usar junto con `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const {
|
|||
|
||||
اگر مؤلفهها را با استفاده از یکی از چارچوبهای ذکر شده در بالا آزمایش میکنید، میتوانید یک پیشتنظیم تعریف کنید که مطمئن شود همه چیز در خارج پیکربندی میشود. این گزینه را نمی توان همراه با `viteConfig`استفاده کرد.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ npm install --save-dev @wdio/browser-runner
|
|||
|
||||
यदि आप ऊपर उल्लिखित रूपरेखाओं में से किसी एक का उपयोग करके घटकों का परीक्षण करते हैं, तो आप एक पूर्व निर्धारित निर्धारित कर सकते हैं जो सुनिश्चित करता है कि सब कुछ बॉक्स से बाहर कॉन्फ़िगर किया गया है। इस विकल्प का उपयोग `viteConfig`के साथ नहीं किया जा सकता है।
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
एक्सपोर्ट const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The Browser runner allows following configurations:
|
|||
|
||||
If you test components using one of the mentioned frameworks above, you can define a preset that ensures everything is configured out of the box. This option can't be used together with `viteConfig`.
|
||||
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact`<br /> __Example:__
|
||||
__Type:__ `vue` | `svelte` | `solid` | `react` | `preact` | `stencil`<br /> __Example:__
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ function Home() {
|
|||
<a href="/docs/component-testing/preact" className={styles.frameworkLogos}><img src="/img/icons/preact.png" alt="Preact" /></a>
|
||||
<a href="/docs/component-testing/solid" className={styles.frameworkLogos}><img src="/img/icons/solidjs.svg" alt="SolidJS" /></a>
|
||||
<a href="/docs/component-testing/lit" className={styles.frameworkLogos}><img src="/img/icons/lit.svg" alt="Lit" /></a>
|
||||
<a href="/docs/component-testing/stencil" className={styles.frameworkLogos}><img src="/img/icons/stencil.svg" alt="Stencil" /></a>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
7
website/static/img/icons/stencil.svg
Normal file
7
website/static/img/icons/stencil.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256px" height="197px" viewBox="0 0 256 197" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<title>Stencil</title>
|
||||
<g>
|
||||
<path d="M193.065354,138.495015 L139.731692,196.923077 L41.5985231,196.923077 L94.4010831,138.495015 L193.065354,138.495015 Z M256,69.2476062 L202.694892,127.675077 L0,127.675077 L53.3053046,69.2476062 L256,69.2476062 Z M214.399015,2.13162821e-14 L161.311508,58.4277662 L62.9326769,58.4277662 L116.098954,2.13162821e-14 L214.399015,2.13162821e-14 Z" fill="#000000"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 651 B |
Loading…
Reference in a new issue