Compare commits
5 Commits
5f3b898b42
...
7dbc947abd
Author | SHA1 | Date |
---|---|---|
![]() |
7dbc947abd | 2 weeks ago |
![]() |
63646f3e1a | 2 weeks ago |
![]() |
a5044cdb44 | 2 weeks ago |
![]() |
760bfb79cb | 2 weeks ago |
![]() |
bc17629a67 | 3 weeks ago |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,79 @@ |
|||||||
|
import { defineConfig, devices } from "@playwright/test"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Read environment variables from file. |
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/ |
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/** |
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/ |
||||||
|
export default defineConfig({ |
||||||
|
testDir: "./src/frontends/tests", |
||||||
|
/* Run tests in files in parallel */ |
||||||
|
fullyParallel: true, |
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */ |
||||||
|
forbidOnly: !!process.env.CI, |
||||||
|
/* Retry on CI only */ |
||||||
|
retries: process.env.CI ? 2 : 0, |
||||||
|
/* Opt out of parallel tests on CI. */ |
||||||
|
workers: process.env.CI ? 1 : undefined, |
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ |
||||||
|
reporter: "html", |
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ |
||||||
|
use: { |
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */ |
||||||
|
baseURL: "http://localhost:4321", |
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ |
||||||
|
trace: "on-first-retry", |
||||||
|
}, |
||||||
|
|
||||||
|
/* Configure projects for major browsers */ |
||||||
|
projects: [ |
||||||
|
{ |
||||||
|
name: "chromium", |
||||||
|
use: { ...devices["Desktop Chrome"] }, |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
name: "firefox", |
||||||
|
use: { ...devices["Desktop Firefox"] }, |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
name: "webkit", |
||||||
|
use: { ...devices["Desktop Safari"] }, |
||||||
|
}, |
||||||
|
|
||||||
|
/* Test against mobile viewports. */ |
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */ |
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
], |
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */ |
||||||
|
webServer: { |
||||||
|
command: "npm run dev", |
||||||
|
url: "http://localhost:4321", |
||||||
|
reuseExistingServer: !process.env.CI, |
||||||
|
}, |
||||||
|
}); |
@ -1,7 +1,5 @@ |
|||||||
<script lang="ts"> |
<script lang="ts"> |
||||||
import HelloWorld from "src/frontends/svelte/HelloWorld.svelte"; |
import HelloWorld from "src/frontends/svelte/HelloWorld.svelte"; |
||||||
|
|
||||||
// |
|
||||||
</script> |
</script> |
||||||
|
|
||||||
<HelloWorld /> |
<HelloWorld /> |
||||||
|
@ -1,9 +1,26 @@ |
|||||||
import React from "react"; |
import React from "react"; |
||||||
|
import useShape from "../../ng-mock/js-land/frontendAdapters/react/useShape"; |
||||||
|
|
||||||
export function HelloWorldReact() { |
export function HelloWorldReact() { |
||||||
|
const state = useShape("Shape1", ""); |
||||||
|
|
||||||
|
window.reactState = state; |
||||||
|
console.log("react render", state); |
||||||
|
|
||||||
|
if (!state) return <>Loading state</>; |
||||||
|
|
||||||
return ( |
return ( |
||||||
<div> |
<div> |
||||||
<p>Hello World from React!</p> |
<p>Hello World from React!</p> |
||||||
|
{state.name} lives at {state.address.street} |
||||||
|
<p></p> |
||||||
|
<button |
||||||
|
onClick={() => { |
||||||
|
state.name = `${state.name} ${state.name}`; |
||||||
|
}} |
||||||
|
> |
||||||
|
Double name |
||||||
|
</button> |
||||||
</div> |
</div> |
||||||
); |
); |
||||||
} |
} |
||||||
|
@ -1,7 +1,19 @@ |
|||||||
<script> |
<script> |
||||||
// Simple Svelte component |
import useShape from "../../ng-mock/js-land/frontendAdapters/svelte/useShape"; |
||||||
|
|
||||||
|
const nestedObject = useShape("Shape1", null); |
||||||
</script> |
</script> |
||||||
|
|
||||||
<div> |
<div> |
||||||
<p>Hello World from Svelte!</p> |
<p>Hello World from Svelte!</p> |
||||||
|
|
||||||
|
<pre>{JSON.stringify($nestedObject, null, 4)}</pre> |
||||||
|
<button |
||||||
|
on:click={() => { |
||||||
|
window.svelteState = $nestedObject; |
||||||
|
$nestedObject.name = $nestedObject.name.toUpperCase(); |
||||||
|
}} |
||||||
|
> |
||||||
|
upper-case name |
||||||
|
</button> |
||||||
</div> |
</div> |
||||||
|
@ -1,22 +0,0 @@ |
|||||||
// Simple Svelte-like component using vanilla JS
|
|
||||||
export function mountSvelteComponent(element: HTMLElement) { |
|
||||||
// Create the DOM structure that would be created by the Svelte component
|
|
||||||
element.innerHTML = ` |
|
||||||
<div style=" |
|
||||||
padding: 20px;
|
|
||||||
border: 2px solid #000000ff;
|
|
||||||
border-radius: 8px; |
|
||||||
margin: 10px; |
|
||||||
background-color: #fff5f5; |
|
||||||
"> |
|
||||||
<h2>Svelte Component</h2> |
|
||||||
<p>Hello World from Svelte!</p> |
|
||||||
</div> |
|
||||||
`;
|
|
||||||
|
|
||||||
return { |
|
||||||
$destroy: () => { |
|
||||||
element.innerHTML = ""; |
|
||||||
}, |
|
||||||
}; |
|
||||||
} |
|
@ -0,0 +1,10 @@ |
|||||||
|
import { test, expect } from "@playwright/test"; |
||||||
|
|
||||||
|
test("components load", async ({ page }) => { |
||||||
|
await page.goto("/"); |
||||||
|
await page.waitForSelector(".vue astro-island"); |
||||||
|
|
||||||
|
await expect(page.locator(".vue .title")).toHaveText("vue"); |
||||||
|
await expect(page.locator(".react .title")).toHaveText("react"); |
||||||
|
await expect(page.locator(".svelte .title")).toHaveText("svelte"); |
||||||
|
}); |
@ -1,9 +1,29 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { WritableComputedRef } from 'vue'; |
||||||
|
import useShape from '../../ng-mock/js-land/frontendAdapters/vue/useShape'; |
||||||
|
|
||||||
|
const shapeObj = useShape("Shape1", "null"); |
||||||
|
|
||||||
|
window.vueState= shapeObj; |
||||||
|
|
||||||
|
const setName = () => { |
||||||
|
shapeObj.value.name = "Bobby" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
|
||||||
<template> |
<template> |
||||||
<div> |
<div> |
||||||
<p>Hello World from Vue!</p> |
<p>Hello World from Vue!</p> |
||||||
</div> |
<div v-if="shapeObj != null"> |
||||||
</template> |
<p>Type is <em>{{shapeObj.type}}</em> with street <em>{{shapeObj.address.street}}</em></p> |
||||||
|
|
||||||
<script setup lang="ts"> |
<button @click="setName"> |
||||||
// Simple Vue component with composition API |
Click to switch name |
||||||
</script> |
</button> |
||||||
|
</div> |
||||||
|
<div v-else> |
||||||
|
Loading state |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -1,8 +0,0 @@ |
|||||||
import { createApp } from "vue"; |
|
||||||
import HelloworldVue from "./HelloWorld.vue"; |
|
||||||
|
|
||||||
export function mountVueComponent(element: HTMLElement) { |
|
||||||
const app = createApp(HelloworldVue); |
|
||||||
app.mount(element); |
|
||||||
return app; |
|
||||||
} |
|
@ -1,6 +1,9 @@ |
|||||||
import type { Diff } from "../types"; |
import type { Diff } from "../types"; |
||||||
|
|
||||||
/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */ |
/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */ |
||||||
export function applyDiff(currentState: object, diff: Diff): object { |
export function applyDiff(currentState: any, diff: Diff) { |
||||||
return JSON.parse(JSON.stringify(diff)); |
const clone = JSON.parse(JSON.stringify(diff)); |
||||||
|
Object.keys(clone).forEach((k) => { |
||||||
|
currentState[k] = clone[k]; |
||||||
|
}); |
||||||
} |
} |
||||||
|
@ -0,0 +1,84 @@ |
|||||||
|
import updateShape from "src/ng-mock/wasm-land/updateShape"; |
||||||
|
import type { Connection, Diff, Scope, Shape } from "../types"; |
||||||
|
import requestShape from "src/ng-mock/wasm-land/requestShape"; |
||||||
|
import { applyDiff } from "./applyDiff"; |
||||||
|
import { batch, deepSignal, watch } from "alien-deepsignals"; |
||||||
|
import { signal as createSignal, computed } from "alien-signals"; |
||||||
|
|
||||||
|
type ReactiveShapeObject = object; |
||||||
|
|
||||||
|
type ShapeObjectSignal = ReturnType< |
||||||
|
typeof createSignal<{ |
||||||
|
content: ReactiveShapeObject | null; |
||||||
|
}> |
||||||
|
>; |
||||||
|
|
||||||
|
const openConnections: Partial<Record<Shape, ShapeObjectSignal>> = {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a signal for a shape object. |
||||||
|
* The function returns a signal of a shape object in the form: |
||||||
|
* `{content: <shapeObject>}` |
||||||
|
**/ |
||||||
|
export function createSignalObjectForShape( |
||||||
|
shape: Shape, |
||||||
|
scope?: Scope, |
||||||
|
poolSignal = true |
||||||
|
) { |
||||||
|
if (poolSignal && openConnections[shape]) return openConnections[shape]; |
||||||
|
|
||||||
|
// DeepSignal has a different API to alien-signals.
|
||||||
|
// Therefore, we need to create a "root signal" wrapper that is
|
||||||
|
// triggered on deepSignal changes.
|
||||||
|
const rootSignal = createSignal<{ |
||||||
|
content: ReactiveShapeObject | null; |
||||||
|
}>({ content: null }); |
||||||
|
|
||||||
|
// State
|
||||||
|
let stopWatcher: any = null; |
||||||
|
let suspendDeepWatcher = false; |
||||||
|
|
||||||
|
const onUpdateFromDb = (diff: Diff, connectionId: Connection["id"]) => { |
||||||
|
const rootSignalValue = rootSignal(); |
||||||
|
console.log("Update received", connectionId, diff); |
||||||
|
// Set new value from applying the diffs to the old value.
|
||||||
|
|
||||||
|
suspendDeepWatcher = true; |
||||||
|
// We need to replace the root signal for now, so this is redundant.
|
||||||
|
batch(() => { |
||||||
|
if (!rootSignalValue) return; // This shouldn't happen but we make the compiler happy.
|
||||||
|
const { content: proxiedShapeObj } = rootSignalValue; |
||||||
|
applyDiff(proxiedShapeObj, diff); |
||||||
|
|
||||||
|
// We put the proxied object into a new object for the root signal to trigger.
|
||||||
|
rootSignal({ content: proxiedShapeObj }); |
||||||
|
}); |
||||||
|
suspendDeepWatcher = false; |
||||||
|
}; |
||||||
|
|
||||||
|
// Do the actual db request.
|
||||||
|
requestShape(shape, scope, onUpdateFromDb).then( |
||||||
|
({ connectionId, shapeObject }) => { |
||||||
|
// Create a deepSignal to put into the vanilla alien-signal.
|
||||||
|
const proxiedShapeObj = deepSignal(shapeObject); |
||||||
|
|
||||||
|
// Notify DB on changes.
|
||||||
|
stopWatcher = watch( |
||||||
|
proxiedShapeObj, |
||||||
|
(newVal, oldVal, onCleanup) => { |
||||||
|
// Don't update when applying changes from db diffs from the db.
|
||||||
|
if (!suspendDeepWatcher) updateShape(connectionId, newVal); |
||||||
|
}, |
||||||
|
{ deep: true } |
||||||
|
); |
||||||
|
|
||||||
|
// Update the root signal.
|
||||||
|
rootSignal({ content: proxiedShapeObj }); |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (poolSignal) openConnections[shape] = rootSignal; |
||||||
|
|
||||||
|
// TODO: Dispose deepSignal root signal disposal.
|
||||||
|
return rootSignal; |
||||||
|
} |
@ -1,31 +0,0 @@ |
|||||||
import handleShapeUpdate from "src/ng-mock/wasm-land/handleShapeUpdate"; |
|
||||||
import type { Connection, Diff, Scope, Shape } from "../types"; |
|
||||||
import handleShapeRequest from "src/ng-mock/wasm-land/handleShapeRequest"; |
|
||||||
import { applyDiff } from "./applyDiff"; |
|
||||||
|
|
||||||
export async function createSignalObjectForShape(shape: Shape, scope?: Scope) { |
|
||||||
const ret: { |
|
||||||
state?: any; |
|
||||||
connectionId?: Connection["id"]; |
|
||||||
update: (diff: Diff) => Promise<void>; |
|
||||||
} = { |
|
||||||
async update(diff) { |
|
||||||
if (!ret.connectionId) |
|
||||||
throw new Error("Connection not established yet for shape" + shape); |
|
||||||
await handleShapeUpdate(ret.connectionId, diff); |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
const onDbUpdate = (diff: Diff) => { |
|
||||||
ret.state = applyDiff(ret.state || {}, diff); |
|
||||||
}; |
|
||||||
|
|
||||||
await handleShapeRequest(shape, onDbUpdate).then( |
|
||||||
({ connectionId, shapeObject }) => { |
|
||||||
ret.state = shapeObject; |
|
||||||
ret.connectionId = connectionId; |
|
||||||
} |
|
||||||
); |
|
||||||
|
|
||||||
return ret; |
|
||||||
} |
|
@ -0,0 +1,22 @@ |
|||||||
|
import { useSignal } from "@gn8/alien-signals-react"; |
||||||
|
import { useMemo, useRef } from "react"; |
||||||
|
import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape"; |
||||||
|
import type { Scope, Shape } from "src/ng-mock/js-land/types"; |
||||||
|
|
||||||
|
// TODO: The universe-signal library causes two renders on change instead of one.
|
||||||
|
// Find a fix
|
||||||
|
|
||||||
|
const useShape = (shape: Shape, scope: Scope) => { |
||||||
|
const signalOfShape = useMemo(() => { |
||||||
|
console.log("react memo called..."); |
||||||
|
return createSignalObjectForShape(shape, scope); |
||||||
|
}, [shape, scope]); |
||||||
|
|
||||||
|
const [{ content: shapeObject }, setShapeObject] = useSignal(signalOfShape); |
||||||
|
|
||||||
|
// We don't need the setter.
|
||||||
|
// The object is recursively proxied and changes are recorded there.
|
||||||
|
return shapeObject; |
||||||
|
}; |
||||||
|
|
||||||
|
export default useShape; |
@ -0,0 +1,16 @@ |
|||||||
|
import { derived } from "svelte/store"; |
||||||
|
import { useSignal } from "@gn8/alien-signals-svelte"; |
||||||
|
import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape"; |
||||||
|
import type { Scope, Shape } from "src/ng-mock/js-land/types"; |
||||||
|
|
||||||
|
const useShape = (shape: Shape, scope: Scope) => { |
||||||
|
const signalOfShape = createSignalObjectForShape(shape, scope); |
||||||
|
|
||||||
|
const writeableStoreForShape = useSignal(signalOfShape); |
||||||
|
|
||||||
|
// Get the content "deepSignal"
|
||||||
|
const content = derived(writeableStoreForShape, (value) => value.content); |
||||||
|
return content; |
||||||
|
}; |
||||||
|
|
||||||
|
export default useShape; |
@ -0,0 +1,14 @@ |
|||||||
|
import { useSignal } from "@gn8/alien-signals-vue"; |
||||||
|
import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape"; |
||||||
|
import type { Scope, Shape } from "src/ng-mock/js-land/types"; |
||||||
|
import { computed } from "vue"; |
||||||
|
const useShape = (shape: Shape, scope: Scope) => { |
||||||
|
const signalOfShape = createSignalObjectForShape(shape, scope); |
||||||
|
|
||||||
|
const refOfShape = useSignal(signalOfShape); |
||||||
|
|
||||||
|
// TODO: Maybe `refOfShape.value.content` works too?
|
||||||
|
return computed(() => refOfShape.value.content); |
||||||
|
}; |
||||||
|
|
||||||
|
export default useShape; |
@ -1,23 +1,30 @@ |
|||||||
import { expect, test } from "vitest"; |
import { expect, test } from "vitest"; |
||||||
import { createSignalObjectForShape } from "../js-land/connector/ngSignals"; |
import { createSignalObjectForShape } from "../js-land/connector/createSignalObjectForShape"; |
||||||
|
|
||||||
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); |
||||||
|
|
||||||
|
// TODO: Redo
|
||||||
test("shape object notification comes back to others", async () => { |
test("shape object notification comes back to others", async () => { |
||||||
const object1 = await createSignalObjectForShape("Shape1"); |
const object1 = createSignalObjectForShape("Shape1"); |
||||||
const object2 = await createSignalObjectForShape("Shape1"); |
const object2 = createSignalObjectForShape("Shape1"); |
||||||
|
|
||||||
|
const object3 = createSignalObjectForShape("Shape2"); |
||||||
|
const object4 = createSignalObjectForShape("Shape2"); |
||||||
|
|
||||||
const object3 = await createSignalObjectForShape("Shape2"); |
wait(50); |
||||||
const object4 = await createSignalObjectForShape("Shape2"); |
|
||||||
|
|
||||||
// Update object 1 and expect object 2 to update as well.
|
// Update object 1 and expect object 2 to update as well.
|
||||||
await object1.update({ name: "Updated name from object1" }); |
object1()!.name = "Updated name from object1"; |
||||||
|
|
||||||
expect(object2.state?.name).toBe("Updated name from object1"); |
wait(30); |
||||||
|
expect(object2()!.name).toBe("Updated name from object1"); |
||||||
|
|
||||||
// Expect object of different shape not to have changed.
|
// Expect object of different shape not to have changed.
|
||||||
expect(object3.state?.name).toBe("Niko's cat"); |
expect(object3().name).toBe("Niko's cat"); |
||||||
|
|
||||||
// Update object 4 and expect object 3 with same shape to have updated.
|
// Update object 4 and expect object 3 with same shape to have updated.
|
||||||
await object4.update({ name: "Updated name from object4" }); |
object4()!.name = "Updated name from object4"; |
||||||
|
|
||||||
expect(object3.state?.name).toBe("Updated name from object4"); |
wait(30); |
||||||
|
expect(object3()!.name).toBe("Updated name from object4"); |
||||||
}); |
}); |
||||||
|
@ -1,13 +1,15 @@ |
|||||||
import * as shapeManager from "./shapeManager"; |
import * as shapeManager from "./shapeManager"; |
||||||
import type { WasmConnection, Diff } from "./types"; |
import type { WasmConnection, Diff } from "./types"; |
||||||
|
|
||||||
export default async function handleShapeUpdate( |
export default async function updateShape( |
||||||
connectionId: WasmConnection["id"], |
connectionId: WasmConnection["id"], |
||||||
diff: Diff |
diff: Diff |
||||||
) { |
) { |
||||||
const connection = shapeManager.connections.get(connectionId); |
const connection = shapeManager.connections.get(connectionId); |
||||||
if (!connection) throw new Error("No Connection found."); |
if (!connection) throw new Error("No Connection found."); |
||||||
|
|
||||||
|
console.log("BACKEND: Received update request from ", connectionId); |
||||||
|
|
||||||
const newState = shapeManager.applyDiff(connection.state, diff); |
const newState = shapeManager.applyDiff(connection.state, diff); |
||||||
connection.state = newState; |
connection.state = newState; |
||||||
|
|
Loading…
Reference in new issue