diff --git a/package-lock.json b/package-lock.json index d28b11c94f5..79bb537e9de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7477,18 +7477,6 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -12234,18 +12222,6 @@ "node": ">=4.0" } }, - "node_modules/degenerator/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", diff --git a/packages/fast-element/package.json b/packages/fast-element/package.json index 05fcb20d3f6..89b0eab15e4 100644 --- a/packages/fast-element/package.json +++ b/packages/fast-element/package.json @@ -117,6 +117,8 @@ "prettier:diff": "prettier --config ../../.prettierrc \"**/*.ts\" --list-different", "eslint": "eslint . --ext .ts", "eslint:fix": "eslint . --ext .ts --fix", + "test:playwright": "playwright test", + "test-server": "npx vite test/ --clearScreen false", "test": "npm run eslint && npm run test-chrome:verbose && npm run doc:ci && npm run doc:exports:ci", "test-node": "nyc --reporter=lcov --reporter=text-summary --report-dir=coverage/node --temp-dir=coverage/.nyc_output mocha --reporter min --exit dist/esm/__test__/setup-node.js './dist/esm/**/*.spec.js'", "test-node:verbose": "nyc --reporter=lcov --reporter=text-summary --report-dir=coverage/node --temp-dir=coverage/.nyc_output mocha --reporter spec --exit dist/esm/__test__/setup-node.js './dist/esm/**/*.spec.js'", diff --git a/packages/fast-element/playwright.config.ts b/packages/fast-element/playwright.config.ts new file mode 100644 index 00000000000..bfae0af8026 --- /dev/null +++ b/packages/fast-element/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: ".", + testMatch: "**/*.pw.spec.ts", + retries: 3, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "npm run test-server", + port: 5173, + reuseExistingServer: true, + }, +}); diff --git a/packages/fast-element/src/components/fast-definitions.ts b/packages/fast-element/src/components/fast-definitions.ts index 0333288e69c..eccfdb5f514 100644 --- a/packages/fast-element/src/components/fast-definitions.ts +++ b/packages/fast-element/src/components/fast-definitions.ts @@ -1,6 +1,6 @@ import { Constructable, isString, KernelServiceId } from "../interfaces.js"; import { Observable } from "../observation/observable.js"; -import { createTypeRegistry, FAST, TypeRegistry } from "../platform.js"; +import { createTypeRegistry, FAST, type TypeRegistry } from "../platform.js"; import { ComposableStyles, ElementStyles } from "../styles/element-styles.js"; import type { ElementViewTemplate } from "../templating/template.js"; import { AttributeConfiguration, AttributeDefinition } from "./attributes.js"; diff --git a/packages/fast-element/src/context.pw.spec.ts b/packages/fast-element/src/context.pw.spec.ts new file mode 100644 index 00000000000..ae339c72634 --- /dev/null +++ b/packages/fast-element/src/context.pw.spec.ts @@ -0,0 +1,456 @@ +import { expect, test } from "@playwright/test"; +import { Context } from "./context.js"; + +test.describe("Context", () => { + test.describe(`create()`, () => { + test(`returns a context that has the specified name`, async () => { + const TestContext = Context.create("TestContext"); + + expect(TestContext.name).toBe("TestContext"); + }); + + test(`returns a context that stringifies its name`, async () => { + const TestContext = Context.create("TestContext"); + const expected = "Context"; + + expect(TestContext.toString()).toBe(expected); + expect(String(TestContext)).toBe(expected); + expect(`${TestContext}`).toBe(expected); + }); + + test(`returns a context that gets the initial value if not handled`, async ({ + page, + }) => { + await page.goto("/"); + + const initialValue = "hello world"; + + const value = await page.evaluate(async initialValue => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext", initialValue); + + const node = document.createElement("div"); + return TestContext.get(node); + }, initialValue); + + expect(value).toBe(initialValue); + }); + + test(`returns a context that can be used for a protocol request`, async ({ + page, + }) => { + await page.goto("/"); + + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + TestContext.handle(parent, event => { + if (event.context === TestContext) { + event.stopImmediatePropagation(); + event.callback(value); + } + }); + + let capture = ""; + TestContext.request(child, response => { + capture = response; + }); + + return capture; + }, value); + + expect(capture).toBe(value); + }); + + test(`returns a context that can be used for a get request`, async ({ page }) => { + await page.goto("/"); + + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + TestContext.handle(parent, event => { + if (event.context === TestContext) { + event.stopImmediatePropagation(); + event.callback(value); + } + }); + + return TestContext.get(child); + }, value); + + expect(capture).toBe(value); + }); + + test(`returns a context that can be used to provide a value`, async ({ + page, + }) => { + await page.goto("/"); + + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + TestContext.provide(parent, value); + + return TestContext.get(child); + }, value); + + expect(capture).toBe(value); + }); + + test(`returns a context that can be used as a decorator`, async ({ page }) => { + test.fixme(true, "Decorator doesn’t work in page.evaluate"); + + await page.goto("/"); + + const value = "hello world"; + const childTest = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context, uniqueElementName } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const elementName = uniqueElementName(); + + class TestElement extends HTMLElement { + @TestContext test: string; + } + + customElements.define(elementName, TestElement); + + const parent = document.createElement("div"); + const child = document.createElement(elementName) as TestElement; + parent.append(child); + + TestContext.handle(parent, event => { + if (event.context === TestContext) { + event.stopImmediatePropagation(); + event.callback(value); + } + }); + + return child.test; + }, value); + + expect(childTest).toBe(value); + }); + }); + + test.describe("for()", () => { + test("returns the same context for successive calls with the same name", async () => { + const ctx1 = Context.for("test"); + const ctx2 = Context.for("test"); + + expect(ctx1).toBe(ctx2); + expect(ctx1.name).toBe("test"); + expect(ctx2.name).toBe("test"); + }); + + test("returns different context for successive calls with different names", async () => { + const ctx1 = Context.for("test1"); + const ctx2 = Context.for("test2"); + + expect(ctx1).not.toBe(ctx2); + expect(ctx1.name).toBe("test1"); + expect(ctx2.name).toBe("test2"); + }); + }); + + test.describe(`get()`, () => { + test(`gets the value for a context`, async ({ page }) => { + await page.goto("/"); + + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.handle( + parent, + event => { + event.stopImmediatePropagation(); + event.callback(value); + }, + TestContext + ); + + return Context.get(child, TestContext); + }, value); + + expect(capture).toBe(value); + }); + }); + + test.describe(`request()`, () => { + test(`makes a protocol request`, async ({ page }) => { + await page.goto("/"); + + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.handle( + parent, + event => { + event.stopImmediatePropagation(); + event.callback(value); + }, + TestContext + ); + + let capture = ""; + Context.request(child, TestContext, response => { + capture = response as string; + }); + + return capture; + }, value); + + expect(capture).toBe(value); + }); + }); + + test.describe(`provide()`, () => { + test(`configures a context value without callbacks`, async ({ page }) => { + await page.goto("/"); + + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.provide(parent, TestContext, value); + + return Context.get(child, TestContext); + }, value); + + expect(capture).toBe(value); + }); + }); + + test.describe(`dispatch()`, () => { + test(`dispatches an event even when the request strategy has been changed`, async ({ + page, + }) => { + await page.goto("/"); + + const wrongValue = "hello world"; + const rightValue = "bye bye"; + const capture = await page.evaluate( + async ([wrongValue, rightValue]) => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.setDefaultRequestStrategy((target, context, callback) => { + callback(wrongValue as any); + }); + + Context.handle( + parent, + event => { + event.stopImmediatePropagation(); + event.callback(rightValue); + }, + TestContext + ); + + let capture = ""; + + Context.dispatch(parent, TestContext, value => { + capture = value; + }); + + return capture; + }, + [wrongValue, rightValue] + ); + + expect(capture).toBe(rightValue); + + // Context.setDefaultRequestStrategy(Context.dispatch); + }); + }); + + test.describe(`defineProperty()`, () => { + test(`defines a property on a target that returns the context value`, async ({ + page, + }) => { + await page.goto("/"); + + const value = "hello world"; + const childTest = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.defineProperty(child, "test", TestContext); + + TestContext.handle(parent, event => { + if (event.context === TestContext) { + event.stopImmediatePropagation(); + event.callback(value); + } + }); + + return (child as any).test; + }, value); + + expect(childTest).toBe(value); + }); + }); + + test.describe(`setDefaultRequestStrategy()`, () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + }); + + test(`changes how request() works`, async ({ page }) => { + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.setDefaultRequestStrategy((target, context, callback) => { + callback(value as any); + }); + + let capture = ""; + + Context.request(parent, TestContext, response => { + capture = response; + }); + + return capture; + }, value); + + expect(capture).toBe(value); + }); + + test(`changes how get() works`, async ({ page }) => { + const value = "hello world"; + const capture = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.setDefaultRequestStrategy((target, context, callback) => { + callback(value as any); + }); + + return Context.get(child, TestContext); + }, value); + + expect(capture).toBe(value); + }); + + test(`changes how defineProperty() works`, async ({ page }) => { + const value = "hello world"; + const childTest = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.append(child); + + Context.setDefaultRequestStrategy((target, context, callback) => { + callback(value as any); + }); + + Context.defineProperty(child, "test", TestContext); + + return (child as any).test; + }, value); + + expect(childTest).toBe(value); + }); + + test(`changes how context decorators work`, async ({ page }) => { + test.fixme(true, "Decorator doesn’t work in page.evaluate"); + + const value = "hello world"; + const childTest = await page.evaluate(async value => { + // @ts-expect-error: Client module. + const { Context, uniqueElementName } = await import("/main.js"); + + const TestContext = Context.create("TestContext"); + const elementName = uniqueElementName(); + + class TestElement extends HTMLElement { + @TestContext test: string; + } + + customElements.define(elementName, TestElement); + + const parent = document.createElement("div"); + const child = document.createElement(elementName); + parent.append(child); + + Context.setDefaultRequestStrategy((target, context, callback) => { + callback(value as any); + }); + + return child.test; + }, value); + + expect(childTest).toBe(value); + }); + }); +}); diff --git a/packages/fast-element/src/context.spec.ts b/packages/fast-element/src/context.spec.ts deleted file mode 100644 index 9fcef64793a..00000000000 --- a/packages/fast-element/src/context.spec.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { expect } from "chai"; -import { Context } from "./context.js"; -import { uniqueElementName } from "./testing/fixture.js"; - -describe("Context", () => { - describe(`create()`, () => { - it(`returns a context that has the specified name`, () => { - const TestContext = Context.create("TestContext"); - - expect(TestContext.name).equal("TestContext"); - }); - - it(`returns a context that stringifies its name`, () => { - const TestContext = Context.create("TestContext"); - const expected = "Context"; - - expect(TestContext.toString()).equal(expected); - expect(String(TestContext)).equal(expected); - expect(`${TestContext}`).equal(expected); - }); - - it(`returns a context that gets the initial value if not handled`, () => { - const initialValue = "hello world"; - const TestContext = Context.create("TestContext", initialValue); - - const node = document.createElement("div"); - const value = TestContext.get(node); - - expect(value).equal(initialValue); - }); - - it(`returns a context that can be used for a protocol request`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - TestContext.handle(parent, event => { - if (event.context === TestContext) { - event.stopImmediatePropagation(); - event.callback(value); - } - }); - - let capture; - TestContext.request(child, response => capture = response); - - expect(capture).equal(value); - }); - - it(`returns a context that can be used for a get request`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - TestContext.handle(parent, event => { - if (event.context === TestContext) { - event.stopImmediatePropagation(); - event.callback(value); - } - }); - - const capture = TestContext.get(child); - - expect(capture).equal(value); - }); - - it(`returns a context that can be used to provide a value`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - TestContext.provide(parent, value); - - const capture = TestContext.get(child); - - expect(capture).equal(value); - }); - - it(`returns a context that can be used as a decorator`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const elementName = uniqueElementName(); - - class TestElement extends HTMLElement { - @TestContext test: string; - } - - customElements.define(elementName, TestElement); - - const parent = document.createElement("div"); - const child = document.createElement(elementName) as TestElement; - parent.append(child); - - TestContext.handle(parent, event => { - if (event.context === TestContext) { - event.stopImmediatePropagation(); - event.callback(value); - } - }); - - expect(child.test).equal(value); - }); - }); - - describe("for()", () => { - it("returns the same context for successive calls with the same name", () => { - const ctx1 = Context.for("test"); - const ctx2 = Context.for("test"); - - expect(ctx1).equals(ctx2); - expect(ctx1.name).equals("test"); - expect(ctx2.name).equals("test"); - }); - - it("returns different context for successive calls with different names", () => { - const ctx1 = Context.for("test1"); - const ctx2 = Context.for("test2"); - - expect(ctx1).not.equals(ctx2); - expect(ctx1.name).equals("test1"); - expect(ctx2.name).equals("test2"); - }); - }); - - describe(`get()`, () => { - it(`gets the value for a context`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.handle(parent, event => { - event.stopImmediatePropagation(); - event.callback(value); - }, TestContext); - - const capture = Context.get(child, TestContext); - - expect(capture).equal(value); - }); - }); - - describe(`request()`, () => { - it(`makes a protocol request`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.handle(parent, event => { - event.stopImmediatePropagation(); - event.callback(value); - }, TestContext); - - let capture; - Context.request(child, TestContext, response => capture = response); - - expect(capture).equal(value); - }); - }); - - describe(`provide()`, () => { - it(`configures a context value without callbacks`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.provide(parent, TestContext, value); - - let capture = Context.get(child, TestContext); - - expect(capture).equal(value); - }); - }); - - describe(`dispatch()`, () => { - it(`dispatches an event even when the request strategy has been changed`, () => { - const wrongValue = "hello world"; - const rightValue = "bye bye"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.setDefaultRequestStrategy((target, context, callback) => { - callback(wrongValue as any); - }); - - Context.handle(parent, event => { - event.stopImmediatePropagation(); - event.callback(rightValue); - }, TestContext); - - let capture; - - Context.dispatch(parent, TestContext, value => { - capture = value; - }); - - expect(capture).equal(rightValue); - - Context.setDefaultRequestStrategy(Context.dispatch); - }); - }); - - describe(`defineProperty()`, () => { - it(`defines a property on a target that returns the context value`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.defineProperty(child, "test", TestContext); - - TestContext.handle(parent, event => { - if (event.context === TestContext) { - event.stopImmediatePropagation(); - event.callback(value); - } - }); - - expect((child as any).test).equal(value); - }); - }); - - describe(`setDefaultRequestStrategy()`, () => { - it(`changes how request() works`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.setDefaultRequestStrategy((target, context, callback) => { - callback(value as any); - }); - - let capture; - - Context.request(parent, TestContext, response => { - capture = response; - }); - - expect(capture).equal(value); - - Context.setDefaultRequestStrategy(Context.dispatch); - }); - - it(`changes how get() works`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.setDefaultRequestStrategy((target, context, callback) => { - callback(value as any); - }); - - let capture = Context.get(child, TestContext); - - expect(capture).equal(value); - - Context.setDefaultRequestStrategy(Context.dispatch); - }); - - it(`changes how defineProperty() works`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.append(child); - - Context.setDefaultRequestStrategy((target, context, callback) => { - callback(value as any); - }); - - Context.defineProperty(child, "test", TestContext); - - expect((child as any).test).equal(value); - - Context.setDefaultRequestStrategy(Context.dispatch); - }); - - it(`changes how context decorators work`, () => { - const value = "hello world"; - const TestContext = Context.create("TestContext"); - const elementName = uniqueElementName(); - - class TestElement extends HTMLElement { - @TestContext test: string; - } - - customElements.define(elementName, TestElement); - - const parent = document.createElement("div"); - const child = document.createElement(elementName) as TestElement; - parent.append(child); - - Context.setDefaultRequestStrategy((target, context, callback) => { - callback(value as any); - }); - - expect(child.test).equal(value); - - Context.setDefaultRequestStrategy(Context.dispatch); - }); - }); -}); diff --git a/packages/fast-element/src/debug.pw.spec.ts b/packages/fast-element/src/debug.pw.spec.ts new file mode 100644 index 00000000000..8b3f5d41150 --- /dev/null +++ b/packages/fast-element/src/debug.pw.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from "@playwright/test"; +import "./debug.js"; +import type { FASTGlobal } from "./interfaces.js"; + +declare const FAST: FASTGlobal; + +test.describe("The debug module", () => { + let keyBase = 1111111111; + + test.describe("when sending errors", () => { + test("expect known error message from known error code", async () => { + const key = keyBase++; + const message = "Test Message."; + const messageLookup = { + [key]: message, + }; + + FAST.addMessages(messageLookup); + + const error = FAST.error(key); + expect(error.message).toBe(message); + }); + + test("expect unknown error message from unknown error code", async () => { + const error = FAST.error(10); + expect(error.message).toBe("Unknown Error"); + }); + + test("formats error messages with interpolated values", async () => { + const key = keyBase++; + const message = "${greeting}. ${question} My name is FAST."; + const messageLookup = { + [key]: message, + }; + + FAST.addMessages(messageLookup); + + const error = FAST.error(key, { + greeting: "Hello", + question: "What is your name?", + }); + expect(error.message).toBe("Hello. What is your name? My name is FAST."); + }); + }); +}); diff --git a/packages/fast-element/src/debug.spec.ts b/packages/fast-element/src/debug.spec.ts deleted file mode 100644 index 8c0ccedba04..00000000000 --- a/packages/fast-element/src/debug.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect } from "chai"; -import "./debug.js"; -import type { FASTGlobal } from "./interfaces.js"; - -declare const FAST: FASTGlobal; - -describe("The debug module", () => { - let keyBase = 1111111111; - - context("when sending errors", () => { - it("expect known error message from known error code", () => { - const key = keyBase++; - const message = "Test Message."; - const messageLookup = { - [key]: message - }; - - FAST.addMessages(messageLookup); - - const error = FAST.error(key); - expect(error.message).equal(message); - }); - - it("expect unknown error message from unknown error code", () => { - const error = FAST.error(10); - expect(error.message).equal("Unknown Error"); - }); - - it("formats error messages with interpolated values", () => { - const key = keyBase++; - const message = "${greeting}. ${question} My name is FAST."; - const messageLookup = { - [key]: message - }; - - FAST.addMessages(messageLookup); - - const error = FAST.error(key, { greeting: "Hello", question: "What is your name?" }); - expect(error.message).equal("Hello. What is your name? My name is FAST."); - }); - }); -}); diff --git a/packages/fast-element/src/dom-policy.pw.spec.ts b/packages/fast-element/src/dom-policy.pw.spec.ts new file mode 100644 index 00000000000..33573bc4340 --- /dev/null +++ b/packages/fast-element/src/dom-policy.pw.spec.ts @@ -0,0 +1,381 @@ +import { expect, test } from "@playwright/test"; +import { DOMPolicy, DOMPolicyOptions } from "./dom-policy.js"; +import { DOM, DOMAspect, DOMSink } from "./dom.js"; + +test.describe("the dom policy helper", () => { + test("can create a policy with a custom trusted types policy", async () => { + let invoked = false; + function createTrustedType() { + const createHTML = html => { + invoked = true; + return html; + }; + + return globalThis.trustedTypes + ? globalThis.trustedTypes.createPolicy("fast-html", { createHTML }) + : { createHTML }; + } + + const policy = DOMPolicy.create({ trustedType: createTrustedType() }); + policy.createHTML("Hello world"); + + expect(invoked).toBe(true); + }); + + test("can create a policy with custom element guards", async ({ page }) => { + await page.goto("/"); + + const created = false; + const invoked = false; + + const newStates = JSON.parse( + await page.evaluate(async data => { + // @ts-expect-error: Client modules. + const { DOM, DOMAspect, DOMPolicy } = await import("./main.js"); + + // @ts-expect-error Client side code. + window.returnData = JSON.parse(data); + + const options = { + guards: { + elements: { + a: { + [DOMAspect.attribute]: { + href: function safeURL( + tagName, + aspect, + aspectName, + sink + ) { + // @ts-expect-error Client side code. + window.returnData.created = true; + return (target, name, value, ...rest) => { + // @ts-expect-error Client side code. + window.returnData.invoked = true; + sink(target, name, value, ...rest); + }; + }, + }, + }, + }, + }, + }; + + const policy = DOMPolicy.create(options); + // @ts-expect-error Client side code. + window.sink = policy.protect( + "a", + DOMAspect.attribute, + "href", + DOM.setAttribute + ); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }, JSON.stringify({ created, invoked })) + ); + + expect(newStates.created).toBe(true); + expect(newStates.invoked).toBe(false); + + const newStatesAfterSink = JSON.parse( + await page.evaluate(() => { + const element = document.createElement("a"); + + // @ts-expect-error Client side code. + window.sink(element, "href", "test"); + + // @ts-expect-error Client side code. + window.returnData.href = element.getAttribute("href"); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }) + ); + + expect(newStatesAfterSink.href).toBe("test"); + expect(newStatesAfterSink.created).toBe(true); + expect(newStatesAfterSink.invoked).toBe(true); + }); + + test("creates policies that fallback to default element guards", async ({ page }) => { + await page.goto("/"); + + const created = 0; + const invoked = 0; + + const newStates = JSON.parse( + await page.evaluate(async data => { + // @ts-expect-error: Client modules. + const { DOM, DOMAspect, DOMPolicy } = await import("./main.js"); + + // @ts-expect-error Client side code. + window.returnData = JSON.parse(data); + + const options = { + guards: { + elements: { + a: { + [DOMAspect.attribute]: { + href: function safeURL( + tagName, + aspect, + aspectName, + sink + ) { + // @ts-expect-error Client side code. + window.returnData.created++; + return (target, name, value, ...rest) => { + // @ts-expect-error Client side code. + window.returnData.invoked++; + sink(target, name, value, ...rest); + }; + }, + }, + }, + }, + }, + }; + + // @ts-expect-error Client side code. + window.policy = DOMPolicy.create(options); + // @ts-expect-error Client side code. + window.sink = window.policy.protect( + "a", + DOMAspect.attribute, + "href", + DOM.setAttribute + ); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }, JSON.stringify({ created, invoked })) + ); + + expect(newStates.created).toBe(1); + expect(newStates.invoked).toBe(0); + + const newStates2 = JSON.parse( + await page.evaluate(() => { + const element = document.createElement("a"); + // @ts-expect-error Client side code. + window.sink(element, "href", "test"); + + // @ts-expect-error Client side code. + window.returnData.href = element.getAttribute("href"); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }) + ); + + expect(newStates2.href).toBe("test"); + expect(newStates2.created).toBe(1); + expect(newStates2.invoked).toBe(1); + + const newStates3 = JSON.parse( + await page.evaluate(async () => { + // @ts-expect-error: Client modules. + const { DOMAspect } = await import("./main.js"); + + function setProperty(node, name, value) { + node[name] = value; + } + + // @ts-expect-error Client side code. + const sink2 = window.policy.protect( + "a", + DOMAspect.property, + "href", + setProperty + ); + + const element2 = document.createElement("a"); + sink2(element2, "href", "https://fast.design/"); + + // @ts-expect-error Client side code. + window.returnData.href = element2.href; + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }) + ); + + expect(newStates3.href).toBe("https://fast.design/"); + expect(newStates3.created).toBe(1); + expect(newStates3.invoked).toBe(1); + }); + + test("can create a policy with custom aspect guards", async ({ page }) => { + await page.goto("/"); + + const created = false; + const invoked = false; + + const newStates = JSON.parse( + await page.evaluate(async data => { + // @ts-expect-error: Client modules. + const { DOMAspect, DOMPolicy } = await import("./main.js"); + + function setProperty(node, name, value) { + node[name] = value; + } + + // @ts-expect-error Client side code. + window.returnData = JSON.parse(data); + + const options: DOMPolicyOptions = { + guards: { + aspects: { + [DOMAspect.property]: { + innerHTML: function safeURL( + tagName, + aspect, + aspectName, + sink + ) { + // @ts-expect-error Client side code. + window.returnData.created = true; + return (target, name, value, ...rest) => { + // @ts-expect-error Client side code. + window.returnData.invoked = true; + sink(target, name, value, ...rest); + }; + }, + }, + }, + }, + }; + + const policy = DOMPolicy.create(options); + // @ts-expect-error Client side code. + window.sink = policy.protect( + "div", + DOMAspect.property, + "innerHTML", + setProperty + ); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }, JSON.stringify({ created, invoked })) + ); + + expect(newStates.created).toBe(true); + expect(newStates.invoked).toBe(false); + + const newStates2 = JSON.parse( + await page.evaluate(() => { + const element = document.createElement("div"); + + // @ts-expect-error Client side code. + window.sink(element, "innerHTML", "test"); + + // @ts-expect-error Client side code. + window.returnData.innerHTML = element.innerHTML; + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }) + ); + + expect(newStates2.innerHTML).toBe("test"); + expect(newStates2.created).toBe(true); + expect(newStates2.invoked).toBe(true); + }); + + test("creates policies that fallback to default aspect guards", async ({ page }) => { + await page.goto("/"); + + const created = 0; + const invoked = 0; + + const newStates = JSON.parse( + await page.evaluate(async data => { + // @ts-expect-error: Client modules. + const { DOM, DOMAspect, DOMPolicy } = await import("./main.js"); + + // @ts-expect-error Client side code. + window.returnData = JSON.parse(data); + + const options: DOMPolicyOptions = { + guards: { + aspects: { + [DOMAspect.attribute]: { + foo: function safeURL(fagName, fspect, fspectName, sink) { + // @ts-expect-error Client side code. + window.returnData.created++; + return (target, name, value, ...rest) => { + // @ts-expect-error Client side code. + window.returnData.invoked++; + sink(target, name, value, ...rest); + }; + }, + }, + }, + }, + }; + + // @ts-expect-error Client side code. + window.policy = DOMPolicy.create(options); + + // @ts-expect-error Client side code. + window.sink = policy.protect( + "a", + DOMAspect.attribute, + "foo", + DOM.setAttribute + ); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }, JSON.stringify({ created, invoked })) + ); + + expect(newStates.created).toBe(1); + expect(newStates.invoked).toBe(0); + + const newStates2 = JSON.parse( + await page.evaluate(() => { + const element = document.createElement("a"); + + // @ts-expect-error Client side code. + window.sink(element, "foo", "test"); + + // @ts-expect-error Client side code. + window.returnData.foo = element.getAttribute("foo"); + + // @ts-expect-error Client side code. + return JSON.stringify(window.returnData); + }) + ); + + expect(newStates2.foo).toBe("test"); + expect(newStates2.created).toBe(1); + expect(newStates2.invoked).toBe(1); + + const hasThrown = await page.evaluate(async () => { + // @ts-expect-error: Client modules. + const { DOMAspect } = await import("./main.js"); + + function setProperty(node, name, value) { + node[name] = value; + } + + try { + // @ts-expect-error Client side code. + window.policy.protect( + "div", + DOMAspect.property, + "innerHTML", + setProperty + ); + return false; + } catch { + return true; + } + }); + + expect(hasThrown).toBe(true); + }); +}); diff --git a/packages/fast-element/src/dom-policy.spec.ts b/packages/fast-element/src/dom-policy.spec.ts deleted file mode 100644 index 7def3224b4b..00000000000 --- a/packages/fast-element/src/dom-policy.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { expect } from "chai"; -import { DOMPolicy, DOMPolicyOptions } from "./dom-policy.js"; -import { DOM, DOMAspect, DOMSink } from "./dom.js"; - -describe("the dom policy helper", () => { - const setProperty = (node, name, value) => node[name] = value; - - it("can create a policy with a custom trusted types policy", () => { - let invoked = false; - function createTrustedType() { - const createHTML = html => { - invoked = true; - return html; - }; - - return globalThis.trustedTypes - ? globalThis.trustedTypes.createPolicy("fast-html", { createHTML }) - : { createHTML }; - } - - const policy = DOMPolicy.create({ trustedType: createTrustedType() }); - policy.createHTML("Hello world"); - - expect(invoked).to.be.true; - }); - - it("can create a policy with custom element guards", () => { - let created = false; - let invoked = false; - const options: DOMPolicyOptions = { - guards: { - elements: { - "a": { - [DOMAspect.attribute]: { - href: function safeURL( - tagName: string | null, - aspect: DOMAspect, - aspectName: string, - sink: DOMSink - ): DOMSink { - created = true; - return (target: Node, name: string, value: string, ...rest: any[]) => { - invoked = true; - sink(target, name, value, ...rest); - }; - } - } - } - } - } - }; - - const policy = DOMPolicy.create(options); - - const sink = policy.protect("a", DOMAspect.attribute, "href", DOM.setAttribute); - - expect(created).to.be.true; - expect(invoked).to.be.false; - - const element = document.createElement("a"); - sink(element, "href", "test"); - - expect(element.getAttribute("href")).to.equal("test"); - expect(created).to.be.true; - expect(invoked).to.be.true; - }); - - it("creates policies that fallback to default element guards", () => { - let created = 0; - let invoked = 0; - const options: DOMPolicyOptions = { - guards: { - elements: { - "a": { - [DOMAspect.attribute]: { - href: function safeURL( - tagName: string | null, - aspect: DOMAspect, - aspectName: string, - sink: DOMSink - ): DOMSink { - created++; - return (target: Node, name: string, value: string, ...rest: any[]) => { - invoked++; - sink(target, name, value, ...rest); - }; - } - } - } - } - } - }; - - const policy = DOMPolicy.create(options); - - const sink = policy.protect("a", DOMAspect.attribute, "href", DOM.setAttribute); - - expect(created).to.equal(1); - expect(invoked).to.equal(0); - - const element = document.createElement("a"); - sink(element, "href", "test"); - - expect(element.getAttribute("href")).to.equal("test"); - expect(created).to.equal(1); - expect(invoked).to.equal(1); - - const sink2 = policy.protect("a", DOMAspect.property, "href", setProperty); - - const element2 = document.createElement("a"); - sink2(element2, "href", "https://fast.design/"); - - expect(element2.href).to.equal("https://fast.design/"); - expect(created).to.equal(1); - expect(invoked).to.equal(1); - }); - - it("can create a policy with custom aspect guards", () => { - let created = false; - let invoked = false; - const options: DOMPolicyOptions = { - guards: { - aspects: { - [DOMAspect.property]: { - innerHTML: function safeURL( - tagName: string | null, - aspect: DOMAspect, - aspectName: string, - sink: DOMSink - ): DOMSink { - created = true; - return (target: Node, name: string, value: string, ...rest: any[]) => { - invoked = true; - sink(target, name, value, ...rest); - }; - } - } - } - } - }; - - const policy = DOMPolicy.create(options); - - const sink = policy.protect("div", DOMAspect.property, "innerHTML", setProperty); - - expect(created).to.be.true; - expect(invoked).to.be.false; - - const element = document.createElement("div"); - sink(element, "innerHTML", "test"); - - expect(element.innerHTML).to.equal("test"); - expect(created).to.be.true; - expect(invoked).to.be.true; - }); - - it("creates policies that fallback to default aspect guards", () => { - let created = 0; - let invoked = 0; - const options: DOMPolicyOptions = { - guards: { - aspects: { - [DOMAspect.attribute]: { - foo: function safeURL( - tagName: string | null, - aspect: DOMAspect, - aspectName: string, - sink: DOMSink - ): DOMSink { - created++; - return (target: Node, name: string, value: string, ...rest: any[]) => { - invoked++; - sink(target, name, value, ...rest); - }; - } - } - } - } - }; - - const policy = DOMPolicy.create(options); - - const sink = policy.protect("a", DOMAspect.attribute, "foo", DOM.setAttribute); - - expect(created).to.equal(1); - expect(invoked).to.equal(0); - - const element = document.createElement("a"); - sink(element, "foo", "test"); - - expect(element.getAttribute("foo")).to.equal("test"); - expect(created).to.equal(1); - expect(invoked).to.equal(1); - - expect(() => { - policy.protect("div", DOMAspect.property, "innerHTML", setProperty); - }).to.throw(); - }); -}); diff --git a/packages/fast-element/src/dom-policy.ts b/packages/fast-element/src/dom-policy.ts index 933290fcbc0..dd09d9e9c3b 100644 --- a/packages/fast-element/src/dom-policy.ts +++ b/packages/fast-element/src/dom-policy.ts @@ -1,5 +1,5 @@ -import { DOMAspect, DOMPolicy, DOMSink } from "./dom.js"; -import { isString, Message, TrustedTypesPolicy } from "./interfaces.js"; +import { DOMAspect, type DOMSink, type DOMPolicy as IDOMPolicy } from "./dom.js"; +import { isString, Message, type TrustedTypesPolicy } from "./interfaces.js"; import { FAST } from "./platform.js"; /** @@ -458,7 +458,7 @@ const DOMPolicy = Object.freeze({ * @param options The options to use in creating the policy. * @returns The newly created DOMPolicy. */ - create(options: DOMPolicyOptions = {}): Readonly { + create(options: DOMPolicyOptions = {}): Readonly { const trustedType = options.trustedType ?? createTrustedType(); const guards = createDOMGuards(options.guards ?? {}, defaultDOMGuards); diff --git a/packages/fast-element/src/metadata.pw.spec.ts b/packages/fast-element/src/metadata.pw.spec.ts new file mode 100644 index 00000000000..417fa947b78 --- /dev/null +++ b/packages/fast-element/src/metadata.pw.spec.ts @@ -0,0 +1,223 @@ +import { expect, test } from "@playwright/test"; +import { Metadata } from "./metadata.js"; +import { emptyArray } from "./platform.js"; + +function decorator(): ClassDecorator { + return (target: any) => target; +} + +test.describe("Metadata", () => { + test.describe(`getDesignParamTypes()`, () => { + test(`returns emptyArray if the class has no constructor or decorators`, () => { + class Foo {} + + const actual = Metadata.getDesignParamTypes(Foo); + + expect(actual).toBe(emptyArray); + }); + + test(`returns emptyArray if the class has a decorator but no constructor`, () => { + class Foo {} + decorator()(Foo); + + const actual = Metadata.getDesignParamTypes(Foo); + + expect(actual).toBe(emptyArray); + }); + + test(`returns emptyArray if the class has no constructor args or decorators`, () => { + class Foo { + constructor() { + return; + } + } + + const actual = Metadata.getDesignParamTypes(Foo); + + expect(actual).toBe(emptyArray); + }); + + test(`returns emptyArray if the class has constructor args but no decorators`, () => { + class Bar {} + class Foo { + constructor(public bar: Bar) {} + } + + const actual = Metadata.getDesignParamTypes(Foo); + + expect(actual).toBe(emptyArray); + }); + + test(`returns emptyArray if the class has constructor args and the decorator is applied via a function call`, () => { + class Bar {} + class Foo { + constructor(public bar: Bar) {} + } + + decorator()(Foo); + const actual = Metadata.getDesignParamTypes(Foo); + + expect(actual).toBe(emptyArray); + }); + + test(`returns an empty mutable array if the class has a decorator but no constructor args`, () => { + test.fixme(); + + class Foo { + constructor() { + return; + } + } + decorator()(Foo); + + const actual = Metadata.getDesignParamTypes(Foo); + + expect(actual).not.toBe(emptyArray); + expect(actual).toHaveLength(0); + }); + + test.describe(`falls back to Object for declarations that cannot be statically analyzed`, () => { + test.fixme(); + + interface ArgCtor {} + + for (const argCtor of [ + class Bar {}, + function () { + return; + }, + () => { + return; + }, + class {}, + {}, + Error, + Array, + class Bar {}.prototype, + class Bar {}.prototype.constructor, + ] as any[]) { + test.fixme(); + + class FooDecoratorInvocation { + constructor(public arg: ArgCtor) {} + } + decorator()(FooDecoratorInvocation); + + test(`FooDecoratorInvocation { constructor(${argCtor.name}) }`, () => { + const actual = Metadata.getDesignParamTypes(FooDecoratorInvocation); + expect(actual).toHaveLength(1); + expect(actual[0]).toBe(Object); + }); + + class FooDecoratorNonInvocation { + constructor(public arg: ArgCtor) {} + } + decorator()(FooDecoratorNonInvocation); + + test(`FooDecoratorNonInvocation { constructor(${argCtor.name}) }`, () => { + const actual = Metadata.getDesignParamTypes(FooDecoratorInvocation); + expect(actual).toHaveLength(1); + expect(actual[0]).toBe(Object); + }); + } + }); + + test.describe(`returns the correct types for valid declarations`, () => { + test.fixme(); + + class Bar {} + class Foo {} + class Baz {} + + test.describe(`decorator invocation`, () => { + test(`Class { constructor(public arg: Bar) }`, () => { + class FooBar { + constructor(public arg: Bar) {} + } + decorator()(FooBar); + + const actual = Metadata.getDesignParamTypes(FooBar); + + expect(actual).toHaveLength(1); + expect(actual[0]).toBe(Bar); + }); + + test(`Class { constructor(public arg1: Bar, public arg2: Foo) }`, () => { + class FooBar { + constructor(public arg1: Bar, public arg2: Foo) {} + } + decorator()(FooBar); + + const actual = Metadata.getDesignParamTypes(FooBar); + + expect(actual).toHaveLength(2); + expect(actual[0]).toBe(Bar); + expect(actual[1]).toBe(Foo); + }); + + test(`Class { constructor(public arg1: Bar, public arg2: Foo, public arg3: Baz) }`, () => { + class FooBar { + constructor( + public arg1: Bar, + public arg2: Foo, + public arg3: Baz + ) {} + } + decorator()(FooBar); + + const actual = Metadata.getDesignParamTypes(FooBar); + + expect(actual).toHaveLength(3); + expect(actual[0]).toBe(Bar); + expect(actual[1]).toBe(Foo); + expect(actual[2]).toBe(Baz); + }); + }); + }); + }); + + test.describe(`getAnnotationParamTypes()`, () => { + test("returns emptyArray if the class has no annotations", () => { + class Foo {} + + const actual = Metadata.getAnnotationParamTypes(Foo); + + expect(actual).toBe(emptyArray); + }); + + test("returns added annotations", () => { + class Foo {} + + const a = Metadata.getOrCreateAnnotationParamTypes(Foo); + a.push("test"); + + const actual = Metadata.getAnnotationParamTypes(Foo); + + expect(actual).toHaveLength(1); + expect(actual[0]).toBe("test"); + }); + }); + + test.describe(`getOrCreateAnnotationParamTypes()`, () => { + test("returns an empty mutable array if the class has no annotations", () => { + class Foo {} + + const actual = Metadata.getOrCreateAnnotationParamTypes(Foo); + + expect(actual).not.toBe(emptyArray); + expect(actual).toHaveLength(0); + }); + + test("returns added annotations", () => { + class Foo {} + + const a = Metadata.getOrCreateAnnotationParamTypes(Foo); + a.push("test"); + + const actual = Metadata.getOrCreateAnnotationParamTypes(Foo); + + expect(actual).toHaveLength(1); + expect(actual[0]).toBe("test"); + }); + }); +}); diff --git a/packages/fast-element/src/platform.pw.spec.ts b/packages/fast-element/src/platform.pw.spec.ts new file mode 100644 index 00000000000..9ce04537663 --- /dev/null +++ b/packages/fast-element/src/platform.pw.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from "@playwright/test"; +import type { FASTGlobal } from "./interfaces.js"; +import { createTypeRegistry, type TypeDefinition } from "./platform.js"; + +declare const FAST: FASTGlobal; + +test.describe("The FAST global", () => { + test.describe("kernel API", () => { + test("can get a lazily defined service by id", async () => { + const id = "test-id"; + const service = {}; + const found = FAST.getById(id, () => service); + + expect(found).toBe(service); + }); + + test("returns the first service defined for an id", async () => { + const id = "test-id-2"; + const service1 = {}; + const service2 = {}; + const found1 = FAST.getById(id, () => service1); + const found2 = FAST.getById(id, () => service2); + + expect(found1).toBe(service1); + expect(found2).toBe(service1); + }); + + test("returns null for optional services", async () => { + const id = "test-id-3"; + const found = FAST.getById(id); + + expect(found).toBeNull(); + }); + }); +}); + +test.describe("TypeRegistry", () => { + test("returns undefined when getting the definition for null", async () => { + const reg = createTypeRegistry(); + const value = reg.getForInstance(null); + + expect(value).toBeUndefined(); + }); + + test("returns undefined when getting the definition for undefined", async () => { + const reg = createTypeRegistry(); + const value = reg.getForInstance(undefined); + + expect(value).toBeUndefined(); + }); +}); diff --git a/packages/fast-element/src/platform.spec.ts b/packages/fast-element/src/platform.spec.ts deleted file mode 100644 index e5644480ac2..00000000000 --- a/packages/fast-element/src/platform.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect } from "chai"; -import type { FASTGlobal } from "./interfaces.js"; -import { createTypeRegistry, TypeDefinition } from "./platform.js"; - -declare const FAST: FASTGlobal; - -describe("The FAST global", () => { - context("kernel API", () => { - it("can get a lazily defined service by id", () => { - const id = 'test-id'; - const service = {}; - const found = FAST.getById(id, () => service); - - expect(found).to.equal(service); - }); - - it("returns the first service defined for an id", () => { - const id = 'test-id-2'; - const service1 = {}; - const service2 = {}; - const found1 = FAST.getById(id, () => service1); - const found2 = FAST.getById(id, () => service2); - - expect(found1).to.equal(service1); - expect(found2).to.equal(service1); - }); - - it("returns null for optional services", () => { - const id = 'test-id-3'; - const found = FAST.getById(id); - - expect(found).to.be.null; - }); - }); -}); - -describe("TypeRegistry", () => { - it("returns undefined when getting the definition for null", () => { - const reg = createTypeRegistry(); - const value = reg.getForInstance(null); - - expect(value).to.be.undefined; - }); - - it("returns undefined when getting the definition for undefined", () => { - const reg = createTypeRegistry(); - const value = reg.getForInstance(undefined); - - expect(value).to.be.undefined; - }); -}); diff --git a/packages/fast-element/src/utilities.pw.spec.ts b/packages/fast-element/src/utilities.pw.spec.ts new file mode 100644 index 00000000000..a35fcb78112 --- /dev/null +++ b/packages/fast-element/src/utilities.pw.spec.ts @@ -0,0 +1,141 @@ +import { expect, test } from "@playwright/test"; + +test.describe("The composedParent function", () => { + test("returns the parent of an element, if it has one", async ({ page }) => { + await page.goto("/"); + + const isParent = await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { composedParent } = await import("./main.js"); + + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.appendChild(child); + + return composedParent(child) === parent; + }); + + expect(isParent).toBe(true); + }); +}); + +test.describe("The composedContains function", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + }); + + test("returns true if the test and reference are the same element", async ({ + page, + }) => { + const contains = await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { composedContains } = await import("./main.js"); + + // This matches the behavior of Node.contains() + const target = document.createElement("div"); + + return composedContains(target, target); + }); + + expect(contains).toBe(true); + }); + + test.describe("that are in the same DOM", () => { + test("returns true if the test is a child of the reference", async ({ page }) => { + const contains = await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { composedContains } = await import("./main.js"); + + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.appendChild(child); + + return composedContains(parent, child); + }); + + expect(contains).toBe(true); + }); + + test("returns false if the test is not a child of the reference", async ({ + page, + }) => { + const contains = await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { composedContains } = await import("./main.js"); + + const parent = document.createElement("div"); + const child = document.createElement("div"); + parent.appendChild(child); + + return composedContains(child, parent); + }); + + expect(contains).toBe(false); + }); + }); + + test.describe("that are not in the same DOM", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { html, ref, FASTElement } = await import("./main.js"); + + class TestElement extends FASTElement { + root; + } + + TestElement.define({ + name: "composed-contains-element", + template: html` +
+ `, + }); + }); + }); + + test("should return true if the element being tested is in a shadow DOM of a child element", async ({ + page, + }) => { + const contains = await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { Updates, composedContains } = await import("./main.js"); + + const parent = document.createElement("div"); + const child = document.createElement("composed-contains-element"); + + parent.appendChild(child); + document.body.appendChild(parent); + + await Updates.next(); + + // @ts-expect-error Client side code. + return composedContains(parent, child.root); + }); + + expect(contains).toBe(true); + }); + + test("should return false if the element being tested is in a shadow DOM that is not attached to a child", async ({ + page, + }) => { + const contains = await page.evaluate(async () => { + // @ts-expect-error Client side module. + const { Updates, composedContains } = await import("./main.js"); + + const parent = document.createElement("div"); + const child = document.createElement("composed-contains-element"); + + document.body.appendChild(parent); + document.body.appendChild(child); + + await Updates.next(); + + // @ts-expect-error Client side code. + return composedContains(parent, child.root); + }); + + expect(contains).toBe(false); + }); + }); +}); diff --git a/packages/fast-element/src/utilities.spec.ts b/packages/fast-element/src/utilities.spec.ts deleted file mode 100644 index 023ab921031..00000000000 --- a/packages/fast-element/src/utilities.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { composedContains, composedParent } from "./utilities.js"; -import { expect } from "chai"; -import { Updates } from "./observation/update-queue.js"; -import { customElement, FASTElement } from "./components/fast-element.js"; -import { observable } from "./observation/observable.js"; -import { html } from "./templating/template.js"; -import { ref } from "./templating/ref.js"; - -describe("The composedParent function", () => { - it("returns the parent of an element, if it has one", () => { - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.appendChild(child); - - const found = composedParent(child); - - expect(found).to.equal(parent); - }); -}); - -@customElement({ - name: "composed-contains-element", - template: html` -
- ` -}) -class TestElement extends FASTElement { - @observable - public root: HTMLElement; -} - -describe("The composedContains function", () => { - it("returns true if the test and reference are the same element", () => { - // This matches the behavior of Node.contains() - const target = document.createElement("div"); - - expect(composedContains(target, target)).to.be.true; - }); - - describe("that are in the same DOM", () => { - it("returns true if the test is a child of the reference", () => { - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.appendChild(child); - - expect(composedContains(parent, child)).to.be.true; - }); - it("returns false if the test is not a child of the reference", () => { - const parent = document.createElement("div"); - const child = document.createElement("div"); - parent.appendChild(child); - - expect(composedContains(child, parent)).to.be.false; - }); - }); - - describe("that are not in the same DOM", () => { - it("should return true if the element being tested is in a shadow DOM of a child element", async () => { - const parent = document.createElement("div"); - const child = document.createElement("composed-contains-element") as TestElement; - - parent.appendChild(child); - document.body.appendChild(parent); - - await Updates.next(); - - expect(composedContains(parent, child.root)).to.be.true; - }); - - it("should return false if the element being tested is in a shadow DOM that is not attached to a child", async () => { - const parent = document.createElement("div"); - const child = document.createElement("composed-contains-element") as TestElement; - - document.body.appendChild(parent); - document.body.appendChild(child); - - await Updates.next(); - - expect(composedContains(parent, child.root)).to.be.false; - }); - }); -}); diff --git a/packages/fast-element/test/index.html b/packages/fast-element/test/index.html new file mode 100644 index 00000000000..8f2f1b0265f --- /dev/null +++ b/packages/fast-element/test/index.html @@ -0,0 +1,11 @@ + + + + + + FAST Element tests + + + + + diff --git a/packages/fast-element/test/main.ts b/packages/fast-element/test/main.ts new file mode 100644 index 00000000000..bdd57879058 --- /dev/null +++ b/packages/fast-element/test/main.ts @@ -0,0 +1,10 @@ +export { customElement, FASTElement } from "../src/components/fast-element.js"; +export { Context } from "../src/context.js"; +export { DOM, DOMAspect } from "../src/dom.js"; +export { DOMPolicy } from "../src/dom-policy.js"; +export { observable } from "../src/observation/observable.js"; +export { Updates } from "../src/observation/update-queue.js"; +export { ref } from "../src/templating/ref.js"; +export { html } from "../src/templating/template.js"; +export { uniqueElementName } from "../src/testing/fixture.js"; +export { composedContains, composedParent } from "../src/utilities.js"; diff --git a/packages/fast-element/test/tsconfig.json b/packages/fast-element/test/tsconfig.json new file mode 100644 index 00000000000..eec0ae627c3 --- /dev/null +++ b/packages/fast-element/test/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "isolatedModules": true, + "paths": { + "@microsoft/fast-element": [ + "../src" + ] + } + }, + "include": [ + "**/*.ts" + ] +} diff --git a/packages/fast-element/test/vite.config.ts b/packages/fast-element/test/vite.config.ts new file mode 100644 index 00000000000..742e8544b21 --- /dev/null +++ b/packages/fast-element/test/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + clearScreen: false, + resolve: { + conditions: ["test"], + }, + server: { + strictPort: true, + }, + build: { + outDir: "./dist", + minify: false, + sourcemap: true, + }, + preview: { + port: 5173, + }, +});