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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ coverage/
/packages/realm/binding/build/
/packages/realm/binding/android/build/
/packages/realm/binding/node/build/
/packages/realm/binding/wasm/build/
/packages/realm/prebuilds/
/packages/realm/binding/android/src/main/java/io/realm/react/Version.java
/packages/realm/binding/android/src/main/jniLibs/
Expand Down
22 changes: 22 additions & 0 deletions contrib/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,28 @@ npm install path/to/realm-js/packages/realm
> [!TIP]
> To run any of the `"scripts"` commands from one of the `package.json` files directly from the root, use the `"name"` value from the target `package.json` as such: `npm run <command name> --workspace <package.json name>`.

### Building for WASM (Browsers)

Follow the steps to install the Emscripten SDK (emsdk) on https://emscripten.org/docs/getting_started/downloads.html.

1. Clone the repository and enter the directory
```
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
```
2. Pull, install and activate the latest version
```
git pull
./emsdk install latest
./emsdk activate latest
```
3. Follow the instructions to setup environment variables for your shell (ex add this to your `~/.zshenv`)
```bash
# Emscripten SDK
export EMSDK_QUIET=1
source "$HOME/<insert EMSDK sub-directory>/emsdk_env.sh"
```

### Cleaning up build files

If you need to clean up build files and other untracked files (except for `node_modules` directories), run the following command from the root directory:
Expand Down
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 },
],
},
}
13 changes: 13 additions & 0 deletions integration-tests/environments/browser/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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:,">
<title>Realm integration tests</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
39 changes: 39 additions & 0 deletions integration-tests/environments/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@realm/browser-tests",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"vite": "vite --port 5173 --force",
"runner": "tsx runner.ts",
"test": "wireit",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"wireit": {
"test": {
"command": "mocha-remote -- concurrently npm:vite npm:runner",
"dependencies": [
"../../../packages/realm:build:ts",
"../../../packages/realm:prebuild-wasm"
]
}
},
"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"
}
}
15 changes: 15 additions & 0 deletions integration-tests/environments/browser/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import puppeteer from 'puppeteer';

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();
if (type === "log" || type === "warn" || type === "error" || type === "debug") {
console[type]('[browser]', msg.text());
}
});

await page.goto("http://localhost:5173/");
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 {
}
*/
187 changes: 187 additions & 0 deletions integration-tests/environments/browser/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import './App.css'

import React, { useEffect, useState, createContext, useContext } from "react";
import { Client as MochaRemoteClient, CustomContext } from "mocha-remote-client";

async function loadTests() {
Object.assign(globalThis, {
environment: { ...context, browser: true },
});
try {
await import("@realm/integration-tests/browser");
} catch (err) {
console.error(err);
throw err;
}
}

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<{
tests: (context: CustomContext) => Promise<void> | void;
}>;

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

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

const client = new MochaRemoteClient({
title: `Browser on ${window.navigator.userAgent}`,
async tests() {
// 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
await loadTests();
},
})

function MochaRemoteProvider({ children, tests }: MochaRemoteProviderProps) {
const [connected, setConnected] = useState(false);
const [status, setStatus] = useState<Status>({ kind: "waiting" });

useEffect(() => {
client.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,
});
});
});
// Remove listeners as the component unmounts
return () => {
client.removeAllListeners("connection");
client.removeAllListeners("disconnection");
client.removeAllListeners("running");
};
}, [setStatus, setConnected, tests]);

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

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;
}
}

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;
}
}

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";
}
}

function ConnectionText(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>) {
const { connected } = useMochaRemoteContext();
return <span {...props}>{getConnectionMessage(connected)}</span>
}

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
Loading
Loading