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

Browser support (take 2) #6821

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Next Next commit
WIP on a vite-based browser test environment
kraenhansen committed Aug 16, 2024
commit 750e37b6fe74faddd3dd2fa4eeced019de84c831
18 changes: 18 additions & 0 deletions integration-tests/environments/browser/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
12 changes: 12 additions & 0 deletions integration-tests/environments/browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Realm integration tests</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions integration-tests/environments/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@realm/browser-tests",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"vite": "vite --port 5173",
"runner": "tsx runner.ts",
"test": "mocha-remote -- concurrently npm:vite npm:runner",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"mocha-remote-client": "^1.12.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"mocha-remote-cli": "^1.12.2",
"puppeteer": "^22.12.0",
"vite": "^5.2.0"
}
}
25 changes: 25 additions & 0 deletions integration-tests/environments/browser/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import puppeteer from 'puppeteer';
// import vite from "vite";
// import { Server as MochaRemoteServer } from "mocha-remote-server";

const DEV_TOOLS = process.env.DEV_TOOLS === "true" || process.env.DEV_TOOLS === "1";

const browser = await puppeteer.launch({ devtools: DEV_TOOLS });
const page = await browser.newPage();

page.on('console', msg => {
const type = msg.type() as string;
if (type === "error" || type === "warn" || type === "debug") {
console[type]('[browser]', msg.text());
} else {
console.log('[browser]', msg.text());
}
});

await new Promise(resolve => setTimeout(resolve, 1000));
await page.goto("http://localhost:5173/");

// Keeping the process alive for the tests to complete
setInterval(() => {
console.log("Waiting for tests to complete");
}, 1000 * 60 * 60);
19 changes: 19 additions & 0 deletions integration-tests/environments/browser/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#connection-text {
margin: 1rem;
justify-self: flex-start;
}

#container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}

/*
#status-emoji {
}

#status-text {
}
*/
27 changes: 27 additions & 0 deletions integration-tests/environments/browser/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { MochaRemoteProvider, ConnectionText, StatusEmoji, StatusText } from "./MochaRemoteProvider";

import './App.css'

async function loadTests() {
describe("harness", () => {
it("loads", () => {
console.log("yay!");
});
});
await import("@realm/integration-tests");
await new Promise(resolve => setTimeout(resolve, 2000));
}

function App() {
return (
<MochaRemoteProvider tests={loadTests}>
<ConnectionText id="connection-text" />
<div id="container">
<StatusEmoji id="status-emoji" />
<StatusText id="status-text" />
</div>
</MochaRemoteProvider>
)
}

export default App
162 changes: 162 additions & 0 deletions integration-tests/environments/browser/src/MochaRemoteProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useEffect, useState, createContext, useContext } from "react";

import { Client, CustomContext } from "mocha-remote-client";

export type { CustomContext };

export type Status =
| {
kind: "waiting";
}
| {
kind: "running";
failures: number;
totalTests: number;
currentTest: string;
currentTestIndex: number;
}
| {
kind: "ended";
failures: number;
totalTests: number;
}

export type MochaRemoteProviderProps = React.PropsWithChildren<{
title?: string;
tests: (context: CustomContext) => void;
}>;

export type MochaRemoteContextValue = {
connected: boolean;
status: Status;
context: CustomContext;
};

export const MochaRemoteContext = createContext<MochaRemoteContextValue>({
connected: false,
status: { kind: "waiting" },
context: {},
});

export function MochaRemoteProvider({ children, tests, title = `Browser on ${window.navigator.userAgent}` }: MochaRemoteProviderProps) {
const [connected, setConnected] = useState(false);
const [status, setStatus] = useState<Status>({ kind: "waiting" });
const [context, setContext] = useState<CustomContext>({});
useEffect(() => {
const client = new Client({
title,
tests(context) {
// Adding an async hook before each test to allow the UI to update
beforeEach("async-pause", () => {
return new Promise<void>((resolve) => setTimeout(resolve));
});
// Require in the tests
tests(context);
// Make the context available to context consumers
setContext(context);
},
})
.on("connection", () => {
setConnected(true);
})
.on("disconnection", () => {
setConnected(false);
})
.on("running", (runner) => {
// TODO: Fix the types for "runner"
if (runner.total === 0) {
setStatus({
kind: "ended",
totalTests: 0,
failures: 0,
});
}

let currentTestIndex = 0;

runner.on("test", (test) => {
setStatus({
kind: "running",
currentTest: test.fullTitle(),
// Compute the current test index - incrementing it if we're running
currentTestIndex: currentTestIndex++,
totalTests: runner.total,
failures: runner.failures,
});
}).on("end", () => {
setStatus({
kind: "ended",
totalTests: runner.total,
failures: runner.failures,
});
});
});

return () => {
client.disconnect();
};
}, [setStatus, setContext]);

return (
<MochaRemoteContext.Provider value={{ status, connected, context }}>
{children}
</MochaRemoteContext.Provider>
);
}

export function useMochaRemoteContext() {
return useContext(MochaRemoteContext);
}

function getStatusEmoji(status: Status) {
if (status.kind === "running") {
return "🏃";
} else if (status.kind === "waiting") {
return "⏳";
} else if (status.kind === "ended" && status.totalTests === 0) {
return "🤷";
} else if (status.kind === "ended" && status.failures > 0) {
return "❌";
} else if (status.kind === "ended") {
return "✅";
} else {
return null;
}
}

export function StatusEmoji(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>) {
const { status } = useMochaRemoteContext();
return <span {...props}>{getStatusEmoji(status)}</span>
}

function getStatusMessage(status: Status) {
if (status.kind === "running") {
return `[${status.currentTestIndex + 1} of ${status.totalTests}] ${status.currentTest}`;
} else if (status.kind === "waiting") {
return "Waiting for server to start tests";
} else if (status.kind === "ended" && status.failures > 0) {
return `${status.failures} tests failed!`;
} else if (status.kind === "ended") {
return "All tests succeeded!";
} else {
return null;
}
}

export function StatusText(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>) {
const { status } = useMochaRemoteContext();
return <span {...props}>{getStatusMessage(status)}</span>
}

function getConnectionMessage(connected: boolean) {
if (connected) {
return "🛜 Connected to the Mocha Remote Server";
} else {
return "🔌 Disconnected from the Mocha Remote Server";
}
}

export function ConnectionText(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>) {
const { connected } = useMochaRemoteContext();
return <span {...props}>{getConnectionMessage(connected)}</span>
}
77 changes: 77 additions & 0 deletions integration-tests/environments/browser/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
: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;
}

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

body {
margin: 0;
display: flex;
flex-direction: column;
min-width: 320px;
min-height: 100vh;
text-align: center;
}

h1 {
font-size: 1.4em;
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;
}
}

#root {
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-between;
}

11 changes: 11 additions & 0 deletions integration-tests/environments/browser/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'

import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
1 change: 1 addition & 0 deletions integration-tests/environments/browser/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
26 changes: 26 additions & 0 deletions integration-tests/environments/browser/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["mocha", "mocha-remote-client"],

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
12 changes: 12 additions & 0 deletions integration-tests/environments/browser/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts", "runner.ts"]
}
7 changes: 7 additions & 0 deletions integration-tests/environments/browser/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()],
})
1,006 changes: 913 additions & 93 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@
"packages/mocha-reporter",
"packages/realm-web-integration-tests",
"integration-tests/tests",
"integration-tests/environments/browser",
"integration-tests/environments/node",
"integration-tests/environments/electron",
"integration-tests/environments/react-native-test-app",