mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 19:59:11 +03:00
chore: drop support for solid component testing (#33523)
This commit is contained in:
parent
649e0e0235
commit
e3ed9fa7c3
@ -40,7 +40,7 @@ test('event should work', async ({ mount }) => {
|
||||
|
||||
## How to get started
|
||||
|
||||
Adding Playwright Test to an existing project is easy. Below are the steps to enable Playwright Test for a React, Vue, Svelte or Solid project.
|
||||
Adding Playwright Test to an existing project is easy. Below are the steps to enable Playwright Test for a React, Vue or Svelte project.
|
||||
|
||||
### Step 1: Install Playwright Test for components for your respective framework
|
||||
|
||||
@ -106,7 +106,6 @@ component is mounted using this script. It can be either a `.js`, `.ts`, `.jsx`
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -168,20 +167,6 @@ test('should work', async ({ mount }) => {
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="app.spec.tsx"
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import App from './App';
|
||||
|
||||
test('should work', async ({ mount }) => {
|
||||
const component = await mount(<App />);
|
||||
await expect(component).toContainText('Learn Solid');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Step 3. Run the tests
|
||||
@ -309,7 +294,6 @@ Provide props to a component when mounted.
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -325,17 +309,6 @@ test('props', async ({ mount }) => {
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('props', async ({ mount }) => {
|
||||
const component = await mount(<Component msg="greetings" />);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
@ -379,7 +352,6 @@ Provide callbacks/events to a component when mounted.
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -395,17 +367,6 @@ test('callback', async ({ mount }) => {
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('callback', async ({ mount }) => {
|
||||
const component = await mount(<Component onClick={() => {}} />);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
@ -449,7 +410,6 @@ Provide children/slots to a component when mounted.
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -465,17 +425,6 @@ test('children', async ({ mount }) => {
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('children', async ({ mount }) => {
|
||||
const component = await mount(<Component>Child</Component>);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
@ -519,7 +468,6 @@ You can use `beforeMount` and `afterMount` hooks to configure your app. This let
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Vue3', value: 'vue3'},
|
||||
]
|
||||
}>
|
||||
@ -554,37 +502,6 @@ You can use `beforeMount` and `afterMount` hooks to configure your app. This let
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="playwright/index.tsx"
|
||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-solid/hooks';
|
||||
import { Router } from '@solidjs/router';
|
||||
|
||||
export type HooksConfig = {
|
||||
enableRouting?: boolean;
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ App, hooksConfig }) => {
|
||||
if (hooksConfig?.enableRouting)
|
||||
return <Router><App /></Router>;
|
||||
});
|
||||
```
|
||||
|
||||
```js title="src/pages/ProductsPage.spec.tsx"
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import type { HooksConfig } from '../playwright';
|
||||
import { ProductsPage } from './pages/ProductsPage';
|
||||
|
||||
test('configure routing through hooks config', async ({ page, mount }) => {
|
||||
const component = await mount<HooksConfig>(<ProductsPage />, {
|
||||
hooksConfig: { enableRouting: true },
|
||||
});
|
||||
await expect(component.getByRole('link')).toHaveAttribute('href', '/products/42');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="vue3">
|
||||
|
||||
```js title="playwright/index.ts"
|
||||
@ -626,7 +543,6 @@ Unmount the mounted component from the DOM. This is useful for testing the compo
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -643,18 +559,6 @@ test('unmount', async ({ mount }) => {
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('unmount', async ({ mount }) => {
|
||||
const component = await mount(<Component/>);
|
||||
await component.unmount();
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
@ -700,7 +604,6 @@ Update props, slots/children, and/or events/callbacks of a mounted component. Th
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -719,20 +622,6 @@ test('update', async ({ mount }) => {
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('update', async ({ mount }) => {
|
||||
const component = await mount(<Component/>);
|
||||
await component.update(
|
||||
<Component msg="greetings" onClick={() => {}}>Child</Component>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
@ -820,7 +709,7 @@ test('example test', async ({ mount, router }) => {
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### What's the difference between `@playwright/test` and `@playwright/experimental-ct-{react,svelte,vue,solid}`?
|
||||
### What's the difference between `@playwright/test` and `@playwright/experimental-ct-{react,svelte,vue}`?
|
||||
|
||||
```js
|
||||
test('…', async ({ mount, page, context }) => {
|
||||
@ -828,13 +717,12 @@ test('…', async ({ mount, page, context }) => {
|
||||
});
|
||||
```
|
||||
|
||||
`@playwright/experimental-ct-{react,svelte,vue,solid}` wrap `@playwright/test` to provide an additional built-in component-testing specific fixture called `mount`:
|
||||
`@playwright/experimental-ct-{react,svelte,vue}` wrap `@playwright/test` to provide an additional built-in component-testing specific fixture called `mount`:
|
||||
|
||||
<Tabs
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
@ -895,22 +783,6 @@ test('should work', async ({ mount }) => {
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="solid">
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import HelloWorld from './HelloWorld';
|
||||
|
||||
test.use({ viewport: { width: 500, height: 500 } });
|
||||
|
||||
test('should work', async ({ mount }) => {
|
||||
const component = await mount(<HelloWorld msg="greetings" />);
|
||||
await expect(component).toContainText('Greetings');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Additionally, it adds some config options you can use in your `playwright-ct.config.{ts,js}`.
|
||||
|
182
package-lock.json
generated
182
package-lock.json
generated
@ -207,6 +207,7 @@
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
|
||||
"integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
@ -233,6 +234,7 @@
|
||||
"version": "7.23.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz",
|
||||
"integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.22.5",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
@ -286,6 +288,7 @@
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
|
||||
"integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
@ -326,6 +329,7 @@
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
|
||||
"integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
@ -345,6 +349,7 @@
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
|
||||
"integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-member-expression-to-functions": "^7.22.15",
|
||||
@ -372,6 +377,7 @@
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
|
||||
"integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
@ -467,6 +473,7 @@
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
|
||||
"integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
},
|
||||
@ -517,6 +524,7 @@
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
|
||||
"integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
},
|
||||
@ -579,6 +587,7 @@
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz",
|
||||
"integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.23.3",
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@ -721,6 +730,7 @@
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz",
|
||||
"integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.22.5",
|
||||
"@babel/helper-create-class-features-plugin": "^7.23.6",
|
||||
@ -754,24 +764,6 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-typescript": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz",
|
||||
"integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
"@babel/helper-validator-option": "^7.22.15",
|
||||
"@babel/plugin-syntax-jsx": "^7.23.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.23.3",
|
||||
"@babel/plugin-transform-typescript": "^7.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
|
||||
@ -1496,10 +1488,6 @@
|
||||
"resolved": "packages/playwright-ct-react17",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@playwright/experimental-ct-solid": {
|
||||
"resolved": "packages/playwright-ct-solid",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@playwright/experimental-ct-svelte": {
|
||||
"resolved": "packages/playwright-ct-svelte",
|
||||
"link": true
|
||||
@ -2616,43 +2604,6 @@
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jsx-dom-expressions": {
|
||||
"version": "0.37.13",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.37.13.tgz",
|
||||
"integrity": "sha512-oAEMMIgU0h1DmHn4ZDaBBFc08nsVJciLq9pF7g0ZdpeIDKfY4zXjXr8+/oBjKhXG8nyomhnTodPjeG+/ZXcWXQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "7.18.6",
|
||||
"@babel/plugin-syntax-jsx": "^7.18.6",
|
||||
"@babel/types": "^7.20.7",
|
||||
"html-entities": "2.3.3",
|
||||
"validate-html-nesting": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.20.12"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
|
||||
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-solid": {
|
||||
"version": "1.8.9",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.8.9.tgz",
|
||||
"integrity": "sha512-1awR1QCoryXtAdnjsrx/eVBTYz+tpHUDOdBXqG9oVV7S0ojf2MV/woR0+8BG+LMXVzIr60oKYzCZ9UZGafxmpg==",
|
||||
"dependencies": {
|
||||
"babel-plugin-jsx-dom-expressions": "^0.37.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -4565,11 +4516,6 @@
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
|
||||
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="
|
||||
},
|
||||
"node_modules/html-reporter": {
|
||||
"resolved": "packages/html-reporter",
|
||||
"link": true
|
||||
@ -5038,17 +4984,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
@ -5362,20 +5297,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/merge-anything": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
|
||||
"integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
|
||||
"dependencies": {
|
||||
"is-what": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -6422,25 +6343,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.4.tgz",
|
||||
"integrity": "sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval-plugins": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.4.tgz",
|
||||
"integrity": "sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"seroval": "^1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@ -6530,29 +6432,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js": {
|
||||
"version": "1.8.11",
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.11.tgz",
|
||||
"integrity": "sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "^1.0.3",
|
||||
"seroval-plugins": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-refresh": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz",
|
||||
"integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/helper-module-imports": "^7.22.15",
|
||||
"@babel/types": "^7.23.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@ -7120,11 +6999,6 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-html-nesting": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/validate-html-nesting/-/validate-html-nesting-1.2.2.tgz",
|
||||
"integrity": "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg=="
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@ -7193,24 +7067,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-solid": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.8.2.tgz",
|
||||
"integrity": "sha512-HcvMs6DTxBaO4kE3psnirPQBCUUdYeQkCNKuB2TpEkJsxb6BGP6/7qkbbCSMxn25PyNdjvzVi1WXi0ou8KPgHw==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@types/babel__core": "^7.20.4",
|
||||
"babel-preset-solid": "^1.8.4",
|
||||
"merge-anything": "^5.1.7",
|
||||
"solid-refresh": "^0.6.3",
|
||||
"vitefu": "^0.2.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.7.2",
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
@ -7995,24 +7851,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-solid": {
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.49.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"vite-plugin-solid": "^2.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.49.0-next",
|
||||
|
@ -1,12 +0,0 @@
|
||||
**/*
|
||||
|
||||
!README.md
|
||||
!LICENSE
|
||||
!cli.js
|
||||
!register.d.ts
|
||||
!register.mjs
|
||||
!registerSource.mjs
|
||||
!index.d.ts
|
||||
!index.js
|
||||
!hooks.d.ts
|
||||
!hooks.mjs
|
@ -1,3 +0,0 @@
|
||||
> **BEWARE** This package is EXPERIMENTAL and does not respect semver.
|
||||
|
||||
Read more at https://playwright.dev/docs/test-components
|
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { program } = require('@playwright/experimental-ct-core/lib/program');
|
||||
|
||||
program.parse(process.argv);
|
24
packages/playwright-ct-solid/hooks.d.ts
vendored
24
packages/playwright-ct-solid/hooks.d.ts
vendored
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JSXElement } from 'solid-js';
|
||||
|
||||
export declare function beforeMount<HooksConfig>(
|
||||
callback: (params: { hooksConfig?: HooksConfig, App: () => JSXElement }) => Promise<void | JSXElement>
|
||||
): void;
|
||||
export declare function afterMount<HooksConfig>(
|
||||
callback: (params: { hooksConfig?: HooksConfig }) => Promise<void>
|
||||
): void;
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const __pw_hooks_before_mount = [];
|
||||
const __pw_hooks_after_mount = [];
|
||||
|
||||
window.__pw_hooks_before_mount = __pw_hooks_before_mount;
|
||||
window.__pw_hooks_after_mount = __pw_hooks_after_mount;
|
||||
|
||||
export const beforeMount = callback => {
|
||||
__pw_hooks_before_mount.push(callback);
|
||||
};
|
||||
|
||||
export const afterMount = callback => {
|
||||
__pw_hooks_after_mount.push(callback);
|
||||
};
|
35
packages/playwright-ct-solid/index.d.ts
vendored
35
packages/playwright-ct-solid/index.d.ts
vendored
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestType, Locator } from '@playwright/experimental-ct-core';
|
||||
|
||||
export interface MountOptions<HooksConfig> {
|
||||
hooksConfig?: HooksConfig;
|
||||
}
|
||||
|
||||
export interface MountResult extends Locator {
|
||||
unmount(): Promise<void>;
|
||||
update(component: JSX.Element): Promise<void>;
|
||||
}
|
||||
|
||||
export const test: TestType<{
|
||||
mount<HooksConfig>(
|
||||
component: JSX.Element,
|
||||
options?: MountOptions<HooksConfig>
|
||||
): Promise<MountResult>;
|
||||
}>;
|
||||
|
||||
export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core';
|
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
|
||||
const path = require('path');
|
||||
|
||||
const defineConfig = (config, ...configs) => {
|
||||
return originalDefineConfig({
|
||||
...config,
|
||||
'@playwright/test': {
|
||||
packageJSON: require.resolve('./package.json'),
|
||||
},
|
||||
'@playwright/experimental-ct-core': {
|
||||
registerSourceFile: path.join(__dirname, 'registerSource.mjs'),
|
||||
frameworkPluginFactory: () => import('vite-plugin-solid').then(plugin => plugin.default()),
|
||||
},
|
||||
}, ...configs);
|
||||
};
|
||||
|
||||
module.exports = { test, expect, devices, defineConfig };
|
@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.49.0-next",
|
||||
"description": "Playwright Component Testing for Solid",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/microsoft/playwright.git"
|
||||
},
|
||||
"homepage": "https://playwright.dev",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./register": {
|
||||
"types": "./register.d.ts",
|
||||
"default": "./register.mjs"
|
||||
},
|
||||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"vite-plugin-solid": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
}
|
||||
}
|
17
packages/playwright-ct-solid/register.d.ts
vendored
17
packages/playwright-ct-solid/register.d.ts
vendored
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export default function pwRegister(components: Record<string, any>): void;
|
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { pwRegister } from './registerSource.mjs';
|
||||
|
||||
export default components => {
|
||||
pwRegister(components);
|
||||
};
|
@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
// This file is injected into the registry as text, no dependencies are allowed.
|
||||
|
||||
import { render as __pwSolidRender, createComponent as __pwSolidCreateComponent } from 'solid-js/web';
|
||||
import __pwH from 'solid-js/h';
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is JsxComponent}
|
||||
*/
|
||||
function isJsxComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
function __pwCreateComponent(value) {
|
||||
return window.__pwTransformObject(value, v => {
|
||||
if (isJsxComponent(v)) {
|
||||
const component = v;
|
||||
const props = component.props ? __pwCreateComponent(component.props) : {};
|
||||
if (typeof component.type === 'string') {
|
||||
const { children, ...propsWithoutChildren } = props;
|
||||
return { result: __pwH(component.type, propsWithoutChildren, children) };
|
||||
}
|
||||
return { result: __pwSolidCreateComponent(component.type, props) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const __pwUnmountKey = Symbol('unmountKey');
|
||||
|
||||
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
let App = () => __pwCreateComponent(component);
|
||||
for (const hook of window.__pw_hooks_before_mount || []) {
|
||||
const wrapper = await hook({ App, hooksConfig });
|
||||
if (wrapper)
|
||||
App = () => wrapper;
|
||||
}
|
||||
|
||||
const unmount = __pwSolidRender(App, rootElement);
|
||||
rootElement[__pwUnmountKey] = unmount;
|
||||
|
||||
for (const hook of window.__pw_hooks_after_mount || [])
|
||||
await hook({ hooksConfig });
|
||||
};
|
||||
|
||||
window.playwrightUnmount = async rootElement => {
|
||||
const unmount = rootElement[__pwUnmountKey];
|
||||
if (!unmount)
|
||||
throw new Error('Component was not mounted');
|
||||
|
||||
unmount();
|
||||
delete rootElement[__pwUnmountKey];
|
||||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
window.playwrightUnmount(rootElement);
|
||||
window.playwrightMount(component, rootElement, {});
|
||||
};
|
2
tests/components/ct-solid/.gitignore
vendored
2
tests/components/ct-solid/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
@ -1,34 +0,0 @@
|
||||
## Usage
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
||||
<title>Solid App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "ct-solid",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.8.2",
|
||||
"solid-js": "^1.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-solid": "^2.6.1"
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig, devices } from '@playwright/experimental-ct-solid';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests',
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: process.env.CI ? 'html' : 'line',
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
ctViteConfig: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Solid App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,19 +0,0 @@
|
||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-solid/hooks';
|
||||
import { Router } from "@solidjs/router";
|
||||
import '../src/assets/index.css';
|
||||
|
||||
export type HooksConfig = {
|
||||
route?: string;
|
||||
routing?: boolean;
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ hooksConfig, App }) => {
|
||||
console.log(`Before mount: ${JSON.stringify(hooksConfig)}`);
|
||||
|
||||
if (hooksConfig?.routing)
|
||||
return <Router><App /></Router>;
|
||||
});
|
||||
|
||||
afterMount<HooksConfig>(async () => {
|
||||
console.log(`After mount`);
|
||||
});
|
@ -1,20 +0,0 @@
|
||||
import { Routes, Route, A } from "@solidjs/router"
|
||||
import logo from './assets/logo.svg';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
|
||||
export default function App() {
|
||||
return <>
|
||||
<header>
|
||||
<img src={logo} alt="logo" width={125} height={125} />
|
||||
<A href="/">Login</A>
|
||||
<A href="/dashboard">Dashboard</A>
|
||||
</header>
|
||||
<Routes>
|
||||
<Route path="/">
|
||||
<Route path="/" component={LoginPage} />
|
||||
<Route path="dashboard" component={DashboardPage} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</>
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -1,20 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #e3e3e3;
|
||||
background-color: #1b1b1d;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,13 +0,0 @@
|
||||
import type { JSX } from "solid-js";
|
||||
|
||||
type ButtonProps = {
|
||||
title: string;
|
||||
onClick?(props: string): void;
|
||||
className?: string;
|
||||
} & Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>;
|
||||
|
||||
export default function Button({ onClick, title, ...attributes }: ButtonProps) {
|
||||
return <button {...attributes} onClick={() => onClick?.('hello')}>
|
||||
{title}
|
||||
</button>
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { type ParentProps } from 'solid-js';
|
||||
|
||||
type DefaultChildrenProps = ParentProps<{}>;
|
||||
|
||||
export default function CheckChildrenProp(props: DefaultChildrenProps) {
|
||||
return <>{'children' in props ? props.children : 'No Children'}</>
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
type CounterProps = {
|
||||
count?: number;
|
||||
onClick?(props: string): void;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
let _remountCount = 1;
|
||||
|
||||
export default function Counter(props: CounterProps) {
|
||||
const [remountCount, setRemountCount] = createSignal(_remountCount++);
|
||||
return <button onClick={() => props.onClick?.('hello')}>
|
||||
<span data-testid="props">{props.count}</span>
|
||||
<span data-testid="remount-count">{remountCount()}</span>
|
||||
{ props.children }
|
||||
</button>
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
type DefaultChildrenProps = {
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export default function DefaultChildren(props: DefaultChildrenProps) {
|
||||
return <div>
|
||||
<h1>Welcome!</h1>
|
||||
<main>
|
||||
{props.children}
|
||||
</main>
|
||||
<footer>
|
||||
Thanks for visiting.
|
||||
</footer>
|
||||
</div>
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export default function EmptyFragment(props: unknown) {
|
||||
Object.assign(window, { props });
|
||||
return <>{[]}</>;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default function MultiRoot() {
|
||||
return <>
|
||||
<div>root 1</div>
|
||||
<div>root 2</div>
|
||||
</>
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
type MultipleChildrenProps = {
|
||||
children?: [any, any, any];
|
||||
}
|
||||
|
||||
export default function MultipleChildren(props: MultipleChildrenProps) {
|
||||
return <div>
|
||||
<header>
|
||||
{props.children?.at(0)}
|
||||
</header>
|
||||
<main>
|
||||
{props.children?.at(1)}
|
||||
</main>
|
||||
<footer>
|
||||
{props.children?.at(2)}
|
||||
</footer>
|
||||
</div>
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
/* @refresh reload */
|
||||
import { render } from 'solid-js/web';
|
||||
import { Router } from "@solidjs/router";
|
||||
import App from './App';
|
||||
import './assets/index.css';
|
||||
|
||||
render(() => <Router><App /></Router>, document.getElementById('root')!);
|
@ -1,3 +0,0 @@
|
||||
export default function DashboardPage() {
|
||||
return <main>Dashboard</main>
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function LoginPage() {
|
||||
return <main>Login</main>
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import Button from '@/components/Button';
|
||||
import DefaultChildren from '@/components/DefaultChildren';
|
||||
|
||||
test('execute callback when the button is clicked', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(
|
||||
<Button
|
||||
title="Submit"
|
||||
onClick={(data) => {
|
||||
messages.push(data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
});
|
||||
|
||||
test('execute callback when a child node is clicked', async ({ mount }) => {
|
||||
let clickFired = false;
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<span onClick={() => (clickFired = true)}>Main Content</span>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await component.getByText('Main Content').click();
|
||||
expect(clickFired).toBeTruthy();
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import Button from '@/components/Button';
|
||||
import DefaultChildren from '@/components/DefaultChildren';
|
||||
import MultipleChildren from '@/components/MultipleChildren';
|
||||
import CheckChildrenProp from '@/components/CheckChildrenProp'
|
||||
|
||||
test('render a default child', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>Main Content</DefaultChildren>
|
||||
);
|
||||
await expect(component).toContainText('Main Content');
|
||||
});
|
||||
|
||||
test('render multiple children', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<div data-testid="one">One</div>
|
||||
<div data-testid="two">Two</div>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component.getByTestId('one')).toContainText('One');
|
||||
await expect(component.getByTestId('two')).toContainText('Two');
|
||||
});
|
||||
|
||||
test('render a component as child', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<Button title="Submit" />
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component).toContainText('Submit');
|
||||
});
|
||||
|
||||
test('render named children', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<MultipleChildren>
|
||||
<div>Header</div>
|
||||
<div>Main Content</div>
|
||||
<div>Footer</div>
|
||||
</MultipleChildren>
|
||||
);
|
||||
await expect(component).toContainText('Header');
|
||||
await expect(component).toContainText('Main Content');
|
||||
await expect(component).toContainText('Footer');
|
||||
});
|
||||
|
||||
test('render string as child', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren>{'string'}</DefaultChildren>);
|
||||
await expect(component).toContainText('string');
|
||||
});
|
||||
|
||||
test('render array as child', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren>{[<h4>{[4]}</h4>,[[<p>[2,3]</p>]]]}</DefaultChildren>);
|
||||
await expect(component.getByRole('heading', { level: 4 })).toHaveText('4');
|
||||
await expect(component.getByRole('paragraph')).toHaveText('[2,3]');
|
||||
});
|
||||
|
||||
test('render number as child', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren>{1337}</DefaultChildren>);
|
||||
await expect(component).toContainText('1337');
|
||||
});
|
||||
|
||||
test('absence of children when children prop is not provided', async ({ mount }) => {
|
||||
const component = await mount(<CheckChildrenProp />);
|
||||
await expect(component).toContainText('No Children');
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import Button from '@/components/Button';
|
||||
import EmptyFragment from '@/components/EmptyFragment';
|
||||
|
||||
test('render props', async ({ mount }) => {
|
||||
const component = await mount(<Button title="Submit" />);
|
||||
await expect(component).toContainText('Submit');
|
||||
});
|
||||
|
||||
test('render attributes', async ({ mount }) => {
|
||||
const component = await mount(<Button className="primary" title="Submit" />);
|
||||
await expect(component).toHaveClass('primary');
|
||||
});
|
||||
|
||||
test('render an empty component', async ({ mount, page }) => {
|
||||
const component = await mount(<EmptyFragment />);
|
||||
expect(await page.evaluate(() => 'props' in window && window.props)).toEqual({});
|
||||
expect(await component.allTextContents()).toEqual(['']);
|
||||
expect(await component.textContent()).toBe('');
|
||||
await expect(component).toHaveText('');
|
||||
});
|
@ -1,14 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import App from '@/App';
|
||||
import type { HooksConfig } from '../playwright';
|
||||
|
||||
test('navigate to a page by clicking a link', async ({ page, mount }) => {
|
||||
const component = await mount<HooksConfig>(<App />, {
|
||||
hooksConfig: { routing: true },
|
||||
});
|
||||
await expect(component.getByRole('main')).toHaveText('Login');
|
||||
await expect(page).toHaveURL('/');
|
||||
await component.getByRole('link', { name: 'Dashboard' }).click();
|
||||
await expect(component.getByRole('main')).toHaveText('Dashboard');
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import Button from '@/components/Button';
|
||||
import MultiRoot from '@/components/MultiRoot';
|
||||
|
||||
test('unmount', async ({ page, mount }) => {
|
||||
const component = await mount(<Button title="Submit" />);
|
||||
await expect(page.locator('#root')).toContainText('Submit');
|
||||
await component.unmount();
|
||||
await expect(page.locator('#root')).not.toContainText('Submit');
|
||||
});
|
||||
|
||||
test('unmount a multi root component', async ({ mount, page }) => {
|
||||
const component = await mount(<MultiRoot />);
|
||||
await expect(page.locator('#root')).toContainText('root 1');
|
||||
await expect(page.locator('#root')).toContainText('root 2');
|
||||
await component.unmount();
|
||||
await expect(page.locator('#root')).not.toContainText('root 1');
|
||||
await expect(page.locator('#root')).not.toContainText('root 2');
|
||||
});
|
||||
|
||||
test('unmount twice throws an error', async ({ mount }) => {
|
||||
const component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
@ -1,115 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import Counter from '@/components/Counter';
|
||||
import DefaultChildren from '@/components/DefaultChildren';
|
||||
|
||||
test('update props without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter count={9001} />);
|
||||
await expect(component.getByTestId('props')).toContainText('9001');
|
||||
|
||||
await component.update(<Counter count={1337} />);
|
||||
await expect(component).not.toContainText('9001');
|
||||
await expect(component.getByTestId('props')).toContainText('1337');
|
||||
|
||||
/**
|
||||
* Ideally toContainText('2') should be toContainText('1')
|
||||
* However it seems impossible to update the props, slots or events of a rendered component
|
||||
*/
|
||||
await expect(component.getByTestId('remount-count')).toContainText('2');
|
||||
});
|
||||
|
||||
test('update child props without remounting', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren><Counter count={9001} /></DefaultChildren>);
|
||||
await expect(component.getByTestId('props')).toContainText('9001');
|
||||
|
||||
await component.update(<DefaultChildren><Counter count={1337} /></DefaultChildren>);
|
||||
await expect(component).not.toContainText('9001');
|
||||
await expect(component.getByTestId('props')).toContainText('1337');
|
||||
|
||||
/**
|
||||
* Ideally toContainText('2') should be toContainText('1')
|
||||
* However it seems impossible to update the props, slots or events of a rendered component
|
||||
*/
|
||||
await expect(component.getByTestId('remount-count')).toContainText('2');
|
||||
});
|
||||
|
||||
test('update callbacks without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter />);
|
||||
|
||||
const messages: string[] = [];
|
||||
await component.update(
|
||||
<Counter
|
||||
onClick={(message) => {
|
||||
messages.push(message);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
|
||||
/**
|
||||
* Ideally toContainText('2') should be toContainText('1')
|
||||
* However it seems impossible to update the props, slots or events of a rendered component
|
||||
*/
|
||||
await expect(component.getByTestId('remount-count')).toContainText('2');
|
||||
});
|
||||
|
||||
test('update child callbacks without remounting', async ({ mount }) => {
|
||||
const component = await mount(<DefaultChildren><Counter /></DefaultChildren>);
|
||||
|
||||
const messages: string[] = [];
|
||||
await component.update(
|
||||
<DefaultChildren>
|
||||
<Counter
|
||||
onClick={(message) => {
|
||||
messages.push(message);
|
||||
}}
|
||||
/>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await component.getByRole('button').click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
|
||||
/**
|
||||
* Ideally toContainText('2') should be toContainText('1')
|
||||
* However it seems impossible to update the props, slots or events of a rendered component
|
||||
*/
|
||||
await expect(component.getByTestId('remount-count')).toContainText('2');
|
||||
});
|
||||
|
||||
test('update children without remounting', async ({ mount }) => {
|
||||
const component = await mount(<Counter>Default Slot</Counter>);
|
||||
await expect(component).toContainText('Default Slot');
|
||||
|
||||
await component.update(<Counter>Test Slot</Counter>);
|
||||
await expect(component).not.toContainText('Default Slot');
|
||||
await expect(component).toContainText('Test Slot');
|
||||
|
||||
/**
|
||||
* Ideally toContainText('2') should be toContainText('1')
|
||||
* However it seems impossible to update the props, slots or events of a rendered component
|
||||
*/
|
||||
await expect(component.getByTestId('remount-count')).toContainText('2');
|
||||
});
|
||||
|
||||
test('update grandchild without remounting', async ({ mount }) => {
|
||||
const component = await mount(
|
||||
<DefaultChildren>
|
||||
<Counter>Default Slot</Counter>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component.getByRole('button')).toContainText('Default Slot');
|
||||
|
||||
await component.update(
|
||||
<DefaultChildren>
|
||||
<Counter>Test Slot</Counter>
|
||||
</DefaultChildren>
|
||||
);
|
||||
await expect(component.getByRole('button')).not.toContainText('Default Slot');
|
||||
await expect(component.getByRole('button')).toContainText('Test Slot');
|
||||
|
||||
/**
|
||||
* Ideally toContainText('2') should be toContainText('1')
|
||||
* However it seems impossible to update the props, slots or events of a rendered component
|
||||
*/
|
||||
await expect(component.getByTestId('remount-count')).toContainText('2');
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": ["vite/client"],
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"*": ["_"],
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import solidPlugin from 'vite-plugin-solid';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
@ -197,11 +197,6 @@ const workspace = new Workspace(ROOT_PATH, [
|
||||
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react17'),
|
||||
files: ['LICENSE'],
|
||||
}),
|
||||
new PWPackage({
|
||||
name: '@playwright/experimental-ct-solid',
|
||||
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-solid'),
|
||||
files: ['LICENSE'],
|
||||
}),
|
||||
new PWPackage({
|
||||
name: '@playwright/experimental-ct-svelte',
|
||||
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-svelte'),
|
||||
|
Loading…
Reference in New Issue
Block a user