Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add support for component integration testing #1977

Draft
wants to merge 65 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
1f754ed
WIP
fwouts Aug 22, 2023
136edf8
WIP
fwouts Aug 25, 2023
7a58d4a
Remove unnecessary extension
fwouts Aug 25, 2023
0068a24
Pass real values
fwouts Aug 25, 2023
8eb27f8
it works!
fwouts Aug 25, 2023
4a02f94
WIP to switch to /preview (lots to clean up)
fwouts Aug 26, 2023
f92e3be
merge
fwouts Aug 26, 2023
806a4dc
Merge branch 'main' into api-example-test
fwouts Aug 26, 2023
f4ced1e
WIP
fwouts Aug 26, 2023
cb56a56
Merge branch 'main' into api-example-test
fwouts Aug 26, 2023
9496bb7
progress
fwouts Aug 26, 2023
b5ab701
remove Accept filtering, use trailing / instead
fwouts Aug 26, 2023
a99b275
WIP
fwouts Aug 26, 2023
5aba8b9
Merge branch 'main' into api-example-test
fwouts Aug 26, 2023
f9790ab
it works! super hacky but promising
fwouts Aug 26, 2023
b99dc66
update
fwouts Aug 26, 2023
227468f
add missing await
fwouts Aug 26, 2023
9af9d04
move from window.__PREVIEWJS_IFRAME__.mount to just mount
fwouts Aug 26, 2023
bd1b518
cleanup
fwouts Aug 26, 2023
01249e0
Merge branch 'main' into api-example-test
fwouts Aug 26, 2023
5fb84c6
revert
fwouts Aug 26, 2023
860d315
fix
fwouts Aug 26, 2023
4d5564b
Merge branch 'main' into api-example-test
fwouts Aug 26, 2023
7d2f1cb
Update index.ts
fwouts Aug 26, 2023
f002b24
make it work again
fwouts Aug 26, 2023
cf751b1
fix
fwouts Aug 26, 2023
54fa4bc
Update messages.ts
fwouts Aug 26, 2023
9b59b10
Update index.ts
fwouts Aug 26, 2023
6472635
Merge branch 'main' into api-example-test
fwouts Aug 26, 2023
0574e88
Update example.spec.tsx
fwouts Aug 26, 2023
48d13b4
remove onServerStart
fwouts Aug 27, 2023
bceb51c
Merge branch 'main' into api-example-test
fwouts Aug 27, 2023
70bf019
fix
fwouts Aug 27, 2023
337d6f5
fix
fwouts Aug 27, 2023
fb2732e
actual fix
fwouts Aug 27, 2023
4bbaed3
separate out window._jsx and window.mount init
fwouts Aug 30, 2023
149d5a6
revert
fwouts Aug 30, 2023
8ce5159
Merge branch 'main' into api-example-test
fwouts Aug 30, 2023
319e94b
fix
fwouts Aug 30, 2023
73fc1c7
Merge branch 'main' into api-example-test
fwouts Aug 30, 2023
e683372
fix
fwouts Aug 30, 2023
a05922c
Merge branch 'main' into api-example-test
fwouts Aug 30, 2023
38edc82
fix
fwouts Aug 30, 2023
af0df48
remove unnecessary stop
fwouts Aug 30, 2023
4099ffc
WIP
fwouts Sep 10, 2023
0b15b52
Merge branch 'main' into api-example-test
fwouts Sep 29, 2023
634d4fd
merge
fwouts Sep 29, 2023
36a0f1c
Merge remote-tracking branch 'origin/main' into api-example-test
fwouts Sep 29, 2023
4e1a2e8
fix reload issue
fwouts Sep 29, 2023
cbe175f
clean up syntax
fwouts Sep 29, 2023
ee228f4
remove-log
fwouts Oct 2, 2023
7fa95cc
Merge branch 'main' into api-example-test
fwouts Oct 2, 2023
0a9f6b5
fix
fwouts Oct 2, 2023
4d0974d
enable passing args
fwouts Oct 2, 2023
83f452c
remove extra statement
fwouts Oct 2, 2023
e3d3fc9
use Playwright test fixtures
fwouts Oct 2, 2023
93ee5a3
move out test fixtures
fwouts Oct 2, 2023
dcada9e
rename
fwouts Oct 2, 2023
1d97e41
simply API further with testInfo
fwouts Oct 2, 2023
341f494
remove usage of __dirname
fwouts Oct 2, 2023
420a67d
fix
fwouts Oct 2, 2023
c001d95
rename
fwouts Oct 2, 2023
f3294e7
replace process.chdir() with config.rootDir
fwouts Oct 3, 2023
76716ba
use plugin factory
fwouts Oct 3, 2023
f9e73c2
clean up tsconfig
fwouts Oct 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
661 changes: 661 additions & 0 deletions component-test/LICENSE

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions component-test/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions component-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@previewjs/component-test",
"version": "0.0.0",
"license": "AGPL-3.0",
"type": "module",
"scripts": {
"dev": "vite",
"e2e-test": "playwright test"
},
"dependencies": {
"@types/lodash": "^4.14.197",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@playwright/test": "^1.38.1",
"@previewjs/core": "workspace:*",
"@previewjs/iframe": "workspace:*",
"@previewjs/plugin-react": "workspace:*",
"@types/fs-extra": "^11.0.1",
"@types/node": "^18.16.14",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.4",
"fs-extra": "^11.1.1",
"playwright": "^1.38.1",
"typescript": "^5.2.2",
"unbuild": "^2.0.0",
"vite": "^4.4.9"
}
}
2 changes: 2 additions & 0 deletions component-test/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({});
1 change: 1 addition & 0 deletions component-test/public/vite.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions component-test/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}
35 changes: 35 additions & 0 deletions component-test/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState } from "react";
import "./App.css";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";

function App({ title }: { title: React.ReactNode }) {
const [count, setCount] = useState(0);

return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>{title}</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
);
}

export default App;
3 changes: 3 additions & 0 deletions component-test/src/Bold.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const Bold = ({ children }: { children: React.ReactNode }) => (
<i>{children}</i>
);
10 changes: 10 additions & 0 deletions component-test/src/Foo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isEqual } from "lodash";
import { Bold } from "./Bold";

export const Foo = () => {
return (
<div>
Hello: <Bold>{isEqual(1, 2)}</Bold>!
</div>
);
};
1 change: 1 addition & 0 deletions component-test/src/assets/react.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions component-test/src/example.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import react from "@previewjs/plugin-react";
import { setupTest } from "./lib";

const test = setupTest(react);

test.describe("example", () => {
test("foo", async ({ page, runInPage }) => {
await runInPage(async (message) => {
const { default: App } = await import("./App");

await mount(<App title={message} />);
}, "hello world");

await page.waitForSelector("text=hello world");
await page.screenshot({
path: "src/example.spec.output.png",
});
});
});
69 changes: 69 additions & 0 deletions component-test/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;

color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;

font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}

a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}

body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}

h1 {
font-size: 3.2em;
line-height: 1.1;
}

button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
90 changes: 90 additions & 0 deletions component-test/src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { test as base } from "@playwright/test";
import type {
FrameworkPluginFactory,
PreviewServer,
Workspace,
} from "@previewjs/core";
import { createWorkspace } from "@previewjs/core";
import "@previewjs/iframe";
import path from "path";

let cache: Map<
FrameworkPluginFactory,
ReturnType<typeof createTest>
> = new Map();

export function setupTest(frameworkPluginFactory: FrameworkPluginFactory) {
const cached = cache.get(frameworkPluginFactory);
if (cached !== undefined) {
return cached;
}
const test = createTest(frameworkPluginFactory);
cache.set(frameworkPluginFactory, test);
return test;
}

function createTest(frameworkPluginFactory: FrameworkPluginFactory) {
return base.extend<
{
runInPage(pageFunction: () => Promise<void>): Promise<void>;
runInPage<Arg>(
pageFunction: (arg: Arg) => Promise<void>,
arg: Arg
): Promise<void>;
},
{ previewServer: PreviewServer; previewWorkspace: Workspace }
>({
previewWorkspace: [
// eslint-disable-next-line no-empty-pattern
async ({}, use, { config }) => {
const workspace = await createWorkspace({
rootDir: config.rootDir,
frameworkPlugins: [frameworkPluginFactory],
});
await use(workspace);
await workspace.dispose();
},
{ scope: "worker" },
],
previewServer: [
async ({ previewWorkspace }, use) => {
const previewServer = await previewWorkspace.startServer();
await use(previewServer);
await previewServer.stop();
},
{ scope: "worker" },
],
runInPage: async (
{ previewWorkspace, previewServer, page },
use,
testInfo
) => {
use(async function runInPage<Arg = never>(
pageFunction: (arg: Arg) => Promise<void>,
arg?: Arg
): Promise<void> {
let resolvePromise!: () => void;
const onRenderDone = new Promise<void>((resolve) => {
resolvePromise = resolve;
});
await page.exposeFunction("__ON_PREVIEWJS_MOUNTED__", resolvePromise);
await page.exposeFunction("__PREVIEWJS_BOOSTRAP_HOOK__", async () => {
await page.evaluate(
async ([pageFunctionStr, arg]) => {
const pageFunction = eval(pageFunctionStr);
await pageFunction(arg);
// @ts-expect-error
window.__ON_PREVIEWJS_MOUNTED__();
},
[pageFunction.toString(), arg] as const
);
});
const currentPath = path
.relative(previewWorkspace.rootDir, path.dirname(testInfo.file))
.replaceAll(path.delimiter, "/");
await page.goto(`${previewServer.url()}/${currentPath}/`);
await onRenderDone;
});
},
});
}
10 changes: 10 additions & 0 deletions component-test/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
1 change: 1 addition & 0 deletions component-test/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions component-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
}
7 changes: 7 additions & 0 deletions component-test/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
6 changes: 6 additions & 0 deletions core/src/previewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ export class Previewer {
kind: "starting",
promise: (async () => {
const router = express.Router();
router.get(/^[^:]*\/$/, async (req, res) => {
res
.status(200)
.set({ "Content-Type": "text/html" })
.end(await this.viteManager!.loadIndexHtml(req.originalUrl));
});
router.get(/^\/.*:[^/]+\/$/, async (req, res, next) => {
if (req.url.includes("?html-proxy")) {
next();
Expand Down