Skip to content

PostMessage Cleanup - TodoMVC example (experimental) #509

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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 resources/shared/todomvc-utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const numberOfItemsToAdd = 100;
Original file line number Diff line number Diff line change
Expand Up @@ -724,3 +724,11 @@ export const defaultTodoText = {
ru: "Кое-что сделать",
emoji: "Something to do 😊",
};

export const defaultLanguage = "en";

export function getTodoText(lang = "en", index) {
const todosSelection = todos[lang];
const currentIndex = index % todosSelection.length;
return todosSelection[currentIndex];
}
24 changes: 14 additions & 10 deletions resources/tests.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { BenchmarkTestStep } from "./benchmark-runner.mjs";
import { todos } from "./translations.mjs";

const numberOfItemsToAdd = 100;
const defaultLanguage = "en";

function getTodoText(lang, index) {
const todosSelection = todos[lang] ?? todos[defaultLanguage];
const currentIndex = index % todosSelection.length;
return todosSelection[currentIndex];
}
import { getTodoText, defaultLanguage } from "./shared/translations.mjs";
import { numberOfItemsToAdd } from "./shared/todomvc-utils.mjs";

export const Suites = [];

Expand Down Expand Up @@ -274,6 +266,18 @@ Suites.push({
],
});

Suites.push({
name: "TodoMVC-WebComponents-PostMessage",
url: "resources/todomvc/vanilla-examples/javascript-web-components/dist/index.html",
tags: ["experimental", "todomvc", "webcomponents"],
disabled: true,
async prepare() {},
type: "remote",
/* config: {
name: "default", // optional param to target non-default tests locally
}, */
});

Suites.push({
name: "TodoMVC-WebComponents-Complex-DOM",
url: "resources/todomvc/vanilla-examples/javascript-web-components-complex/dist/index.html",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
<link rel="stylesheet" href="styles/header.css" />
<link rel="stylesheet" href="styles/footer.css" />
<!-- load these first, so that they are registered by the time the app components needs them -->
<script type="module" src="components/todo-topbar/todo-topbar.component.js"></script>
<script type="module" src="components/todo-list/todo-list.component.js"></script>
<script type="module" src="components/todo-bottombar/todo-bottombar.component.js"></script>
<script type="module" src="components/todo-app/todo-app.component.js"></script>
<script type="module" src="src/components/todo-topbar/todo-topbar.component.js"></script>
<script type="module" src="src/components/todo-list/todo-list.component.js"></script>
<script type="module" src="src/components/todo-bottombar/todo-bottombar.component.js"></script>
<script type="module" src="src/components/todo-app/todo-app.component.js"></script>
</head>

<body>
Expand All @@ -26,5 +26,6 @@
<p class="footer-text">Press 'enter' to add the todo.</p>
<p class="footer-text">Double-click to edit a todo</p>
</footer>
<script type="module" src="src/index.mjs"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import template from "./todo-app.template.js";
import { useRouter } from "../../hooks/useRouter.js";

import globalStyles from "../../styles/global.constructable.js";
import appStyles from "../../styles/app.constructable.js";
import mainStyles from "../../styles/main.constructable.js";
import globalStyles from "../../../styles/global.constructable.js";
import appStyles from "../../../styles/app.constructable.js";
import mainStyles from "../../../styles/main.constructable.js";
class TodoApp extends HTMLElement {
#isReady = false;
#data = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import template from "./todo-bottombar.template.js";

import globalStyles from "../../styles/global.constructable.js";
import bottombarStyles from "../../styles/bottombar.constructable.js";
import globalStyles from "../../../styles/global.constructable.js";
import bottombarStyles from "../../../styles/bottombar.constructable.js";

class TodoBottombar extends HTMLElement {
static get observedAttributes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import template from "./todo-item.template.js";
import { useDoubleClick } from "../../hooks/useDoubleClick.js";
import { useKeyListener } from "../../hooks/useKeyListener.js";

import globalStyles from "../../styles/global.constructable.js";
import itemStyles from "../../styles/todo-item.constructable.js";
import globalStyles from "../../../styles/global.constructable.js";
import itemStyles from "../../../styles/todo-item.constructable.js";

class TodoItem extends HTMLElement {
static get observedAttributes() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import template from "./todo-list.template.js";
import TodoItem from "../todo-item/todo-item.component.js";

import globalStyles from "../../styles/global.constructable.js";
import listStyles from "../../styles/todo-list.constructable.js";
import globalStyles from "../../../styles/global.constructable.js";
import listStyles from "../../../styles/todo-list.constructable.js";

class TodoList extends HTMLElement {
static get observedAttributes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import template from "./todo-topbar.template.js";
import { useKeyListener } from "../../hooks/useKeyListener.js";
import { nanoid } from "../../utils/nanoid.js";

import globalStyles from "../../styles/global.constructable.js";
import topbarStyles from "../../styles/topbar.constructable.js";
import globalStyles from "../../../styles/global.constructable.js";
import topbarStyles from "../../../styles/topbar.constructable.js";

class TodoTopbar extends HTMLElement {
static get observedAttributes() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BenchmarkConnector } from "./speedometer-utils/benchmark.mjs";
import suites, { appName, appVersion } from "./workload-test.mjs";

/*
Paste below into dev console for manual testing:
window.addEventListener("message", (event) => console.log(event.data));
window.postMessage({ id: "todomvc-postmessage-1.0.0", key: "benchmark-connector", type: "benchmark-suite", name: "default" }, "*");
*/
const benchmarkConnector = new BenchmarkConnector(suites, appName, appVersion);
benchmarkConnector.connect();
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* eslint-disable no-case-declarations */
import { TestRunner } from "./test-runner.mjs";
import { Params } from "./params.mjs";

/**
* BenchmarkStep
*
* A single test step, with a common interface to interact with.
*/
export class BenchmarkStep {
constructor(name, run) {
this.name = name;
this.run = run;
}

async runAndRecord(params, suite, test, callback) {
const testRunner = new TestRunner(null, null, params, suite, test, callback);
const result = await testRunner.runTest();
return result;
}
}

/**
* BenchmarkSuite
*
* A single test suite that contains one or more test steps.
*/
export class BenchmarkSuite {
constructor(name, tests) {
this.name = name;
this.tests = tests;
}

record(_test, syncTime, asyncTime) {
const total = syncTime + asyncTime;
const results = {
tests: { Sync: syncTime, Async: asyncTime },
total: total,
};

return results;
}

async runAndRecord(params, onProgress) {
const measuredValues = {
tests: {},
total: 0,
};
const suiteStartLabel = `suite-${this.name}-start`;
const suiteEndLabel = `suite-${this.name}-end`;

performance.mark(suiteStartLabel);

for (const test of this.tests) {
const result = await test.runAndRecord(params, this, test, this.record);
measuredValues.tests[test.name] = result;
measuredValues.total += result.total;
onProgress?.(test.name);
}

performance.mark(suiteEndLabel);
performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel);

return {
type: "suite-tests-complete",
status: "success",
result: measuredValues,
suitename: this.name,
};
}
}

/** **********************************************************************
* BenchmarkConnector
*
* postMessage is used to communicate between app and benchmark.
* When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions.
*
* A prepare script within the apps appends window.name and window.version from the package.json file.
* The appId is build by appending name-version
* It's used as an additional safe-guard to ensure the correct app responds to a message.
*************************************************************************/
export class BenchmarkConnector {
constructor(suites, name, version) {
this.suites = suites;
this.name = name;
this.version = version;

if (!name || !version)
console.warn("No name or version supplied, to create a unique appId");

this.appId = name && version ? `${name}-${version}` : -1;
this.onMessage = this.onMessage.bind(this);
}

async onMessage(event) {
if (event.data.id !== this.appId || event.data.key !== "benchmark-connector")
return;

switch (event.data.type) {
case "benchmark-suite":
const params = new Params(new URLSearchParams(window.location.search));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we're gonna propagate location.search from the main frame to iframe?
Maybe it's easier this one just grabbed params out of top.location.search instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the benefit of doing it this way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we don't have to propagate / forward the query string from the main document to the iframe's document.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nicer to explicitly forward this for debugging purposes. One would easily see the full URL including params for the frame in the network log and could open that separately.

Either approach would be fine.

const suite = this.suites[event.data.name];
if (!suite)
console.error(`Suite with the name of "${event.data.name}" not found!`);
const { result } = await suite.runAndRecord(params, (test) => this.sendMessage({ type: "step-complete", status: "success", appId: this.appId, name: this.name, test }));
this.sendMessage({ type: "suite-complete", status: "success", appId: this.appId, result });
this.disconnect();
break;
default:
console.error(`Message data type not supported: ${event.data.type}`);
}
}

sendMessage(message) {
window.top.postMessage(message, "*");
}

connect() {
window.addEventListener("message", this.onMessage);
this.sendMessage({ type: "app-ready", status: "success", appId: this.appId });
}

disconnect() {
window.removeEventListener("message", this.onMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Helper Methods
*
* Various methods that are extracted from the Page class.
*/
export function getParent(lookupStartNode, path) {
lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode;
const parent = path.reduce((root, selector) => {
const node = root.querySelector(selector);
return node.shadowRoot ?? node;
}, lookupStartNode);

return parent;
}

export function getElement(selector, path = [], lookupStartNode = document) {
const element = getParent(lookupStartNode, path).querySelector(selector);
return element;
}

export function getAllElements(selector, path = [], lookupStartNode = document) {
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
return elements;
}

export function forceLayout() {
const rect = document.body.getBoundingClientRect();
const e = document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0);
return e;
}
Loading