diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f2739ea..7d919d9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -5,28 +5,27 @@ name: Node.js CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: - runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm ci - - run: npx playwright install --with-deps - - run: npm run build --if-present - - run: npm test + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npx playwright install --with-deps + - run: npm run build --if-present + - run: npm test diff --git a/tests/edgeCases.spec.js b/tests/edgeCases.spec.js new file mode 100644 index 0000000..3830d62 --- /dev/null +++ b/tests/edgeCases.spec.js @@ -0,0 +1,303 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("oc-client : edge cases", () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + window.originalConsoleLog = console.log; + console.log = () => {}; + }); + }); + + test.afterEach(async ({ page }) => { + await page.evaluate(() => { + console.log = window.originalConsoleLog; + delete window.originalConsoleLog; + }); + }); + + test("should handle build with special characters in parameters", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const options = { + baseUrl: "https://example.com", + name: "test-component", + version: "1.0.0", + parameters: { + message: "Hello & Welcome!", + query: "search=test+value", + encoded: "already%20encoded", + symbols: "!@#$%^&*()", + }, + }; + + return { + html: oc.build(options), + }; + }); + + expect(result.html).toContain("oc-component"); + expect(result.html).toContain("href="); + expect(result.html).toContain("Hello%20%26%20Welcome!"); + expect(result.html).toContain("search%3Dtest%2Bvalue"); + }); + + test("should handle build with empty parameters", async ({ page }) => { + const result = await page.evaluate(() => { + const options = { + baseUrl: "https://example.com", + name: "test-component", + version: "1.0.0", + parameters: {}, + }; + + return { + html: oc.build(options), + }; + }); + + expect(result.html).toContain("oc-component"); + expect(result.html).toContain("test-component/1.0.0/"); + expect(result.html).not.toContain("?"); + }); + + test("should handle build without version", async ({ page }) => { + const result = await page.evaluate(() => { + const options = { + baseUrl: "https://example.com", + name: "test-component", + }; + + return { + html: oc.build(options), + }; + }); + + expect(result.html).toContain("oc-component"); + expect(result.html).toContain("test-component/"); + expect(result.html).toContain('href="https://example.com/test-component/"'); + }); + + test("should handle build with baseUrl having trailing slash", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const options1 = { + baseUrl: "https://example.com/", + name: "test-component", + version: "1.0.0", + }; + + const options2 = { + baseUrl: "https://example.com", + name: "test-component", + version: "1.0.0", + }; + + return { + html1: oc.build(options1), + html2: oc.build(options2), + }; + }); + + expect(result.html1).toContain("https://example.com/test-component/1.0.0/"); + expect(result.html2).toContain("https://example.com/test-component/1.0.0/"); + expect(result.html1).toBe(result.html2); + }); + + test("should handle renderNestedComponent with jQuery-like object", async ({ + page, + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const element = document.createElement("oc-component"); + element.setAttribute("href", "https://example.com/component"); + element.setAttribute("id", "test-component"); + document.body.appendChild(element); + + const jqueryLikeObject = [element]; + jqueryLikeObject[0] = element; + + let callbackCalled = false; + + oc.renderNestedComponent(jqueryLikeObject, () => { + callbackCalled = true; + }); + + setTimeout(() => { + resolve({ + callbackCalled, + elementHasDataRendering: element.hasAttribute("data-rendering"), + elementDataRendering: element.getAttribute("data-rendering"), + }); + }, 100); + }); + }); + + expect(result.elementHasDataRendering).toBe(true); + expect(result.elementDataRendering).toBe("true"); + }); + + test("should handle renderNestedComponent with already rendered component", async ({ + page, + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const element = document.createElement("oc-component"); + element.setAttribute("href", "https://example.com/component"); + element.setAttribute("id", "test-component"); + element.setAttribute("data-rendered", "true"); + document.body.appendChild(element); + + let callbackCalled = false; + const startTime = Date.now(); + + oc.renderNestedComponent(element, () => { + callbackCalled = true; + const endTime = Date.now(); + resolve({ + callbackCalled, + timeTaken: endTime - startTime, + wasDelayed: endTime - startTime >= 400, + }); + }); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.wasDelayed).toBe(true); + }); + + test("should handle renderNestedComponent with currently rendering component", async ({ + page, + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const element = document.createElement("oc-component"); + element.setAttribute("href", "https://example.com/component"); + element.setAttribute("id", "test-component"); + element.setAttribute("data-rendering", "true"); + document.body.appendChild(element); + + let callbackCalled = false; + const startTime = Date.now(); + + oc.renderNestedComponent(element, () => { + callbackCalled = true; + const endTime = Date.now(); + resolve({ + callbackCalled, + timeTaken: endTime - startTime, + wasDelayed: endTime - startTime >= 400, + }); + }); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.wasDelayed).toBe(true); + }); + + test("should handle load with jQuery-like placeholder", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const placeholder = document.createElement("div"); + placeholder.id = "test-placeholder"; + document.body.appendChild(placeholder); + + const jqueryLikePlaceholder = [placeholder]; + jqueryLikePlaceholder[0] = placeholder; + + oc.load( + jqueryLikePlaceholder, + "https://example.com/component", + (component) => { + resolve({ + callbackCalled: true, + componentExists: !!component, + placeholderHasOcComponent: + placeholder.querySelector("oc-component") !== null, + placeholderInnerHTML: placeholder.innerHTML, + }); + }, + ); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.placeholderHasOcComponent).toBe(true); + expect(result.placeholderInnerHTML).toContain("oc-component"); + expect(result.placeholderInnerHTML).toContain( + 'href="https://example.com/component"', + ); + }); + + test("should handle getAction with missing component", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + oc.getAction({ component: "non-existent-component" }) + .then((props) => { + resolve({ + success: true, + props: props, + }); + }) + .catch((error) => { + resolve({ + success: false, + error: error.message || error, + }); + }); + }); + }); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + }); + + test("should handle multiple rapid renderUnloadedComponents calls", async ({ + page, + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const component1 = document.createElement("oc-component"); + component1.setAttribute("href", "https://example.com/component1"); + component1.setAttribute("id", "component1"); + document.body.appendChild(component1); + + const component2 = document.createElement("oc-component"); + component2.setAttribute("href", "https://example.com/component2"); + component2.setAttribute("id", "component2"); + document.body.appendChild(component2); + + let renderCallCount = 0; + const originalRenderNestedComponent = oc.renderNestedComponent; + + oc.renderNestedComponent = (component, callback) => { + renderCallCount++; + component.setAttribute("data-rendered", "true"); + callback(); + }; + + oc.renderUnloadedComponents(); + + setTimeout(() => { + oc.renderNestedComponent = originalRenderNestedComponent; + resolve({ + renderCallCount, + component1HasDataRendered: component1.hasAttribute("data-rendered"), + component2HasDataRendered: component2.hasAttribute("data-rendered"), + }); + }, 100); + }); + }); + + expect(result.renderCallCount).toBeGreaterThan(0); + expect(result.component1HasDataRendered).toBe(true); + expect(result.component2HasDataRendered).toBe(true); + }); +}); diff --git a/tests/errorHandling.spec.js b/tests/errorHandling.spec.js new file mode 100644 index 0000000..885d203 --- /dev/null +++ b/tests/errorHandling.spec.js @@ -0,0 +1,244 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("oc-client : error handling", () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + window.originalConsoleLog = console.log; + window.originalConsoleError = console.error; + window.capturedLogs = []; + window.capturedErrors = []; + + console.log = (...args) => { + window.capturedLogs.push(args.join(" ")); + }; + console.error = (...args) => { + window.capturedErrors.push(args.join(" ")); + }; + }); + }); + + test.afterEach(async ({ page }) => { + await page.evaluate(() => { + console.log = window.originalConsoleLog; + console.error = window.originalConsoleError; + delete window.originalConsoleLog; + delete window.originalConsoleError; + delete window.capturedLogs; + delete window.capturedErrors; + }); + }); + + test("should handle events.on without callback", async ({ page }) => { + const result = await page.evaluate(() => { + try { + oc.events.on("test:event"); + return { success: true, error: null }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + expect(result.success).toBe(false); + expect(result.error).toBe("Callback is required"); + }); + + test("should handle events.off with string event", async ({ page }) => { + const result = await page.evaluate(() => { + let eventFired = false; + const handler = () => { + eventFired = true; + }; + + oc.events.on("test:remove", handler); + oc.events.fire("test:remove"); + const firedAfterAdd = eventFired; + + eventFired = false; + oc.events.off("test:remove", handler); + oc.events.fire("test:remove"); + const firedAfterRemove = eventFired; + + return { + firedAfterAdd, + firedAfterRemove, + }; + }); + + expect(result.firedAfterAdd).toBe(true); + expect(result.firedAfterRemove).toBe(false); + }); + + test("should handle events.off with array of events", async ({ page }) => { + const result = await page.evaluate(() => { + let event1Fired = false; + let event2Fired = false; + + const handler1 = () => { + event1Fired = true; + }; + const handler2 = () => { + event2Fired = true; + }; + + oc.events.on("test:event1", handler1); + oc.events.on("test:event2", handler2); + + oc.events.fire("test:event1"); + oc.events.fire("test:event2"); + const bothFiredAfterAdd = event1Fired && event2Fired; + + event1Fired = false; + event2Fired = false; + + oc.events.off(["test:event1", "test:event2"]); + oc.events.fire("test:event1"); + oc.events.fire("test:event2"); + const bothFiredAfterRemove = event1Fired || event2Fired; + + return { + bothFiredAfterAdd, + bothFiredAfterRemove, + }; + }); + + expect(result.bothFiredAfterAdd).toBe(true); + expect(result.bothFiredAfterRemove).toBe(false); + }); + + test("should handle events.off without handler (remove all)", async ({ + page, + }) => { + const result = await page.evaluate(() => { + let handler1Called = false; + let handler2Called = false; + + const handler1 = () => { + handler1Called = true; + }; + const handler2 = () => { + handler2Called = true; + }; + + oc.events.on("test:removeAll", handler1); + oc.events.on("test:removeAll", handler2); + + oc.events.fire("test:removeAll"); + const bothCalledAfterAdd = handler1Called && handler2Called; + + handler1Called = false; + handler2Called = false; + + oc.events.off("test:removeAll"); + oc.events.fire("test:removeAll"); + const anyCalledAfterRemove = handler1Called || handler2Called; + + return { + bothCalledAfterAdd, + anyCalledAfterRemove, + }; + }); + + expect(result.bothCalledAfterAdd).toBe(true); + expect(result.anyCalledAfterRemove).toBe(false); + }); + + test("should handle render with unsupported template type", async ({ + page, + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const compiledViewInfo = { + type: "unsupported-template-type", + src: "https://example.com/template.js", + key: "test-key", + }; + + oc.render(compiledViewInfo, { test: "data" }, (error, html) => { + resolve({ + hasError: !!error, + errorMessage: error, + html: html, + }); + }); + }); + }); + + expect(result.hasError).toBe(true); + expect(result.errorMessage).toContain("not supported"); + expect(result.errorMessage).toContain("unsupported-template-type"); + expect(result.html).toBeUndefined(); + }); + + test("should handle render with empty response", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const compiledViewInfo = { + type: "handlebars", + src: "https://example.com/template.js", + key: "test-key", + }; + + const model = { + __oc_emptyResponse: true, + test: "data", + }; + + oc.render(compiledViewInfo, model, (error, html) => { + resolve({ + hasError: !!error, + errorMessage: error, + html: html, + }); + }); + }); + }); + + expect(result.hasError).toBe(false); + expect(result.errorMessage).toBeNull(); + expect(result.html).toBe(""); + }); + + test("should handle renderByHref with empty string href", async ({ + page, + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + oc.renderByHref("", (error, data) => { + resolve({ + hasError: !!error, + errorMessage: error, + data: data, + }); + }); + }); + }); + + expect(result.hasError).toBe(true); + expect(result.errorMessage).toContain("Href parameter missing"); + expect(result.data).toBeUndefined(); + }); + + test("should handle load with null placeholder", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + let callbackCalled = false; + + oc.load(null, "https://example.com/component", () => { + callbackCalled = true; + }); + + setTimeout(() => { + resolve({ + callbackCalled, + }); + }, 100); + }); + }); + + expect(result.callbackCalled).toBe(false); + }); +}); diff --git a/tests/loader.spec.js b/tests/loader.spec.js new file mode 100644 index 0000000..b19647a --- /dev/null +++ b/tests/loader.spec.js @@ -0,0 +1,197 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("oc-client : loader (LJS)", () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + window.originalConsoleLog = console.log; + console.log = () => {}; + }); + }); + + test.afterEach(async ({ page }) => { + await page.evaluate(() => { + console.log = window.originalConsoleLog; + delete window.originalConsoleLog; + }); + }); + + test("should load JavaScript files successfully", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const testScript = ` + window.testLoadedScript = true; + window.testScriptData = { loaded: true, timestamp: Date.now() }; + `; + const blob = new Blob([testScript], { type: "application/javascript" }); + const url = URL.createObjectURL(blob); + + ljs.load(url, () => { + resolve({ + scriptLoaded: !!window.testLoadedScript, + scriptData: window.testScriptData, + loadedResources: Array.from(ljs.loaded), + }); + }); + }); + }); + + expect(result.scriptLoaded).toBe(true); + expect(result.scriptData).toEqual( + expect.objectContaining({ loaded: true }), + ); + expect(result.loadedResources.length).toBeGreaterThan(0); + }); + + test("should handle loading errors gracefully", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const nonExistentUrl = "https://nonexistent-domain-12345.com/script.js"; + let errorHandled = false; + + ljs.onError(() => { + errorHandled = true; + }); + + ljs.load(nonExistentUrl, () => { + resolve({ + errorHandled, + errorCount: ljs.errors.size, + }); + }); + + setTimeout(() => { + resolve({ + errorHandled, + errorCount: ljs.errors.size, + }); + }, 1000); + }); + }); + + expect(result.errorCount).toBeGreaterThan(0); + }); + + test("should load multiple resources concurrently", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const scripts = [ + "window.script1Loaded = true;", + "window.script2Loaded = true;", + "window.script3Loaded = true;", + ]; + + const urls = scripts.map((script) => { + const blob = new Blob([script], { type: "application/javascript" }); + return URL.createObjectURL(blob); + }); + + ljs.load(urls, () => { + resolve({ + script1Loaded: !!window.script1Loaded, + script2Loaded: !!window.script2Loaded, + script3Loaded: !!window.script3Loaded, + loadedCount: ljs.loaded.size, + }); + }); + }); + }); + + expect(result.script1Loaded).toBe(true); + expect(result.script2Loaded).toBe(true); + expect(result.script3Loaded).toBe(true); + expect(result.loadedCount).toBeGreaterThan(2); + }); + + test("should load CSS files successfully", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const cssContent = ` + .test-css-class { + color: red; + font-size: 16px; + } + `; + const blob = new Blob([cssContent], { type: "text/css" }); + const url = URL.createObjectURL(blob); + + ljs.load(url, () => { + const stylesheets = Array.from(document.styleSheets); + const hasTestStyle = stylesheets.some((sheet) => { + try { + return Array.from(sheet.cssRules || []).some((rule) => + rule.selectorText?.includes("test-css-class"), + ); + } catch (e) { + return false; + } + }); + + resolve({ + cssLoaded: hasTestStyle, + loadedCount: ljs.loaded.size, + }); + }); + }); + }); + + expect(result.loadedCount).toBeGreaterThan(0); + }); + + test("should handle module loading", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const moduleScript = ` + export const testModule = { name: 'test', version: '1.0.0' }; + window.moduleLoaded = true; + `; + const blob = new Blob([moduleScript], { + type: "application/javascript", + }); + const url = URL.createObjectURL(blob); + + ljs.load( + url, + () => { + resolve({ + moduleLoaded: !!window.moduleLoaded, + loadedCount: ljs.loaded.size, + }); + }, + true, + ); + }); + }); + + expect(result.loadedCount).toBeGreaterThan(0); + }); + + test("should not reload already loaded resources", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const testScript = + "window.loadCounter = (window.loadCounter || 0) + 1;"; + const blob = new Blob([testScript], { type: "application/javascript" }); + const url = URL.createObjectURL(blob); + + ljs.load(url, () => { + const firstLoadCount = window.loadCounter; + + ljs.load(url, () => { + resolve({ + firstLoadCount, + secondLoadCount: window.loadCounter, + loadedResourcesCount: ljs.loaded.size, + }); + }); + }); + }); + }); + + expect(result.firstLoadCount).toBe(1); + expect(result.secondLoadCount).toBe(1); + }); +}); diff --git a/tests/registerTemplates.spec.js b/tests/registerTemplates.spec.js new file mode 100644 index 0000000..065e463 --- /dev/null +++ b/tests/registerTemplates.spec.js @@ -0,0 +1,175 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("oc-client : registerTemplates", () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + window.originalConsoleLog = console.log; + console.log = () => {}; + }); + }); + + test.afterEach(async ({ page }) => { + await page.evaluate(() => { + console.log = window.originalConsoleLog; + delete window.originalConsoleLog; + }); + }); + + test("should register single template", async ({ page }) => { + const result = await page.evaluate(() => { + const template = { + type: "custom-template", + externals: ["https://example.com/custom-lib.js"], + }; + + const registeredTemplates = oc.registerTemplates(template); + + return { + templateRegistered: !!registeredTemplates["custom-template"], + externals: registeredTemplates["custom-template"]?.externals, + templateCount: Object.keys(registeredTemplates).length, + }; + }); + + expect(result.templateRegistered).toBe(true); + expect(result.externals).toEqual(["https://example.com/custom-lib.js"]); + expect(result.templateCount).toBeGreaterThan(0); + }); + + test("should register multiple templates", async ({ page }) => { + const result = await page.evaluate(() => { + const templates = [ + { + type: "template-one", + externals: ["https://example.com/lib1.js"], + }, + { + type: "template-two", + externals: [ + "https://example.com/lib2.js", + "https://example.com/lib3.js", + ], + }, + ]; + + const registeredTemplates = oc.registerTemplates(templates); + + return { + template1Registered: !!registeredTemplates["template-one"], + template2Registered: !!registeredTemplates["template-two"], + template1Externals: registeredTemplates["template-one"]?.externals, + template2Externals: registeredTemplates["template-two"]?.externals, + totalTemplates: Object.keys(registeredTemplates).length, + }; + }); + + expect(result.template1Registered).toBe(true); + expect(result.template2Registered).toBe(true); + expect(result.template1Externals).toEqual(["https://example.com/lib1.js"]); + expect(result.template2Externals).toEqual([ + "https://example.com/lib2.js", + "https://example.com/lib3.js", + ]); + expect(result.totalTemplates).toBeGreaterThan(1); + }); + + test("should not overwrite existing template by default", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const originalTemplate = { + type: "existing-template", + externals: ["https://example.com/original.js"], + }; + + const newTemplate = { + type: "existing-template", + externals: ["https://example.com/new.js"], + }; + + oc.registerTemplates(originalTemplate); + const afterFirst = oc.registerTemplates(newTemplate); + + return { + externalsAfterSecond: afterFirst["existing-template"]?.externals, + isOriginalPreserved: + JSON.stringify(afterFirst["existing-template"]?.externals) === + JSON.stringify(["https://example.com/original.js"]), + }; + }); + + expect(result.isOriginalPreserved).toBe(true); + expect(result.externalsAfterSecond).toEqual([ + "https://example.com/original.js", + ]); + }); + + test("should handle template with no externals", async ({ page }) => { + const result = await page.evaluate(() => { + const template = { + type: "no-externals-template", + }; + + const registeredTemplates = oc.registerTemplates(template); + + return { + templateRegistered: !!registeredTemplates["no-externals-template"], + externals: registeredTemplates["no-externals-template"]?.externals, + }; + }); + + expect(result.templateRegistered).toBe(true); + expect(result.externals).toBeUndefined(); + }); + + test("should handle empty template array", async ({ page }) => { + const result = await page.evaluate(() => { + const originalCount = Object.keys(oc.registerTemplates([])).length; + const afterEmpty = oc.registerTemplates([]); + const finalCount = Object.keys(afterEmpty).length; + + return { + originalCount, + finalCount, + countsEqual: originalCount === finalCount, + }; + }); + + expect(result.countsEqual).toBe(true); + }); + + test("should return all registered templates", async ({ page }) => { + const result = await page.evaluate(() => { + const template1 = { + type: "return-test-1", + externals: ["lib1.js"], + }; + + const template2 = { + type: "return-test-2", + externals: ["lib2.js"], + }; + + oc.registerTemplates(template1); + const allTemplates = oc.registerTemplates(template2); + + return { + hasTemplate1: !!allTemplates["return-test-1"], + hasTemplate2: !!allTemplates["return-test-2"], + template1Externals: allTemplates["return-test-1"]?.externals, + template2Externals: allTemplates["return-test-2"]?.externals, + isObject: typeof allTemplates === "object", + }; + }); + + expect(result.hasTemplate1).toBe(true); + expect(result.hasTemplate2).toBe(true); + expect(result.template1Externals).toEqual(["lib1.js"]); + expect(result.template2Externals).toEqual(["lib2.js"]); + expect(result.isObject).toBe(true); + }); +}); diff --git a/tests/require.spec.js b/tests/require.spec.js new file mode 100644 index 0000000..274ff27 --- /dev/null +++ b/tests/require.spec.js @@ -0,0 +1,178 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("oc-client : require", () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + window.originalConsoleLog = console.log; + console.log = () => {}; + }); + }); + + test.afterEach(async ({ page }) => { + await page.evaluate(() => { + console.log = window.originalConsoleLog; + delete window.originalConsoleLog; + delete window.TestLibrary; + delete window.MyNamespace; + }); + }); + + test("should require library with namespace", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const libraryScript = ` + window.TestLibrary = { + version: '1.0.0', + utils: { + format: (str) => str.toUpperCase() + } + }; + `; + const blob = new Blob([libraryScript], { + type: "application/javascript", + }); + const url = URL.createObjectURL(blob); + + oc.require("TestLibrary", url, (lib) => { + resolve({ + libraryLoaded: !!lib, + version: lib?.version, + utilsAvailable: typeof lib?.utils?.format === "function", + formatResult: lib?.utils?.format("hello"), + }); + }); + }); + }); + + expect(result.libraryLoaded).toBe(true); + expect(result.version).toBe("1.0.0"); + expect(result.utilsAvailable).toBe(true); + expect(result.formatResult).toBe("HELLO"); + }); + + test("should require library with nested namespace", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const libraryScript = ` + window.MyNamespace = window.MyNamespace || {}; + window.MyNamespace.SubModule = { + data: { test: true }, + methods: { + getValue: () => 'nested-value' + } + }; + `; + const blob = new Blob([libraryScript], { + type: "application/javascript", + }); + const url = URL.createObjectURL(blob); + + oc.require(["MyNamespace", "SubModule"], url, (subModule) => { + resolve({ + subModuleLoaded: !!subModule, + hasData: !!subModule?.data, + testValue: subModule?.data?.test, + methodResult: subModule?.methods?.getValue(), + }); + }); + }); + }); + + expect(result.subModuleLoaded).toBe(true); + expect(result.hasData).toBe(true); + expect(result.testValue).toBe(true); + expect(result.methodResult).toBe("nested-value"); + }); + + test("should handle require without namespace", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const libraryScript = ` + window.globalLibraryLoaded = true; + window.globalLibraryData = { timestamp: Date.now() }; + `; + const blob = new Blob([libraryScript], { + type: "application/javascript", + }); + const url = URL.createObjectURL(blob); + + oc.require(url, (result) => { + resolve({ + callbackCalled: true, + resultIsUndefined: result === undefined, + globalLibraryLoaded: !!window.globalLibraryLoaded, + globalDataExists: !!window.globalLibraryData, + }); + }); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.resultIsUndefined).toBe(true); + expect(result.globalLibraryLoaded).toBe(true); + expect(result.globalDataExists).toBe(true); + }); + + test("should not reload already available library", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + window.TestLibrary = { + loadCount: 1, + increment: function () { + this.loadCount++; + }, + }; + + const libraryScript = ` + if (window.TestLibrary) { + window.TestLibrary.increment(); + } + `; + const blob = new Blob([libraryScript], { + type: "application/javascript", + }); + const url = URL.createObjectURL(blob); + + oc.require("TestLibrary", url, (lib) => { + resolve({ + libraryLoaded: !!lib, + loadCount: lib?.loadCount, + }); + }); + }); + }); + + expect(result.libraryLoaded).toBe(true); + expect(result.loadCount).toBe(1); + }); + + test("should handle missing namespace gracefully", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + const libraryScript = ` + window.SomeOtherLibrary = { loaded: true }; + `; + const blob = new Blob([libraryScript], { + type: "application/javascript", + }); + const url = URL.createObjectURL(blob); + + oc.require("NonExistentLibrary", url, (lib) => { + resolve({ + libraryLoaded: !!lib, + isUndefined: lib === undefined, + otherLibraryExists: !!window.SomeOtherLibrary, + }); + }); + }); + }); + + expect(result.libraryLoaded).toBe(false); + expect(result.isUndefined).toBe(true); + expect(result.otherLibraryExists).toBe(true); + }); +}); diff --git a/tests/utilities.spec.js b/tests/utilities.spec.js new file mode 100644 index 0000000..46bf675 --- /dev/null +++ b/tests/utilities.spec.js @@ -0,0 +1,189 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("oc-client : utility functions", () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + window.originalConsoleLog = console.log; + console.log = () => {}; + }); + }); + + test.afterEach(async ({ page }) => { + await page.evaluate(() => { + console.log = window.originalConsoleLog; + delete window.originalConsoleLog; + }); + }); + + test("should handle addStylesToHead with multiple styles", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const initialStyleCount = document.head.querySelectorAll("style").length; + + oc.addStylesToHead(".test1 { color: red; }"); + oc.addStylesToHead(".test2 { color: blue; }"); + oc.addStylesToHead(".test3 { color: green; }"); + + const finalStyleCount = document.head.querySelectorAll("style").length; + const styles = Array.from(document.head.querySelectorAll("style")); + const lastThreeStyles = styles.slice(-3); + + return { + initialCount: initialStyleCount, + finalCount: finalStyleCount, + addedCount: finalStyleCount - initialStyleCount, + lastThreeContents: lastThreeStyles.map((style) => style.textContent), + }; + }); + + expect(result.addedCount).toBe(3); + expect(result.lastThreeContents).toEqual([ + ".test1 { color: red; }", + ".test2 { color: blue; }", + ".test3 { color: green; }", + ]); + }); + + test("should handle addStylesToHead with empty styles", async ({ page }) => { + const result = await page.evaluate(() => { + const initialStyleCount = document.head.querySelectorAll("style").length; + + oc.addStylesToHead(""); + oc.addStylesToHead(" "); + + const finalStyleCount = document.head.querySelectorAll("style").length; + const styles = Array.from(document.head.querySelectorAll("style")); + const lastTwoStyles = styles.slice(-2); + + return { + initialCount: initialStyleCount, + finalCount: finalStyleCount, + addedCount: finalStyleCount - initialStyleCount, + lastTwoContents: lastTwoStyles.map((style) => style.textContent), + }; + }); + + expect(result.addedCount).toBe(2); + expect(result.lastTwoContents).toEqual(["", " "]); + }); + + test("should handle addStylesToHead with complex CSS", async ({ page }) => { + const result = await page.evaluate(() => { + const complexCSS = ` + .complex-selector > .child:nth-child(2n+1) { + background: linear-gradient(45deg, #ff0000, #00ff00); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transform: translateX(10px) rotate(5deg); + } + @media (max-width: 768px) { + .responsive { display: none; } + } + `; + + const initialStyleCount = document.head.querySelectorAll("style").length; + oc.addStylesToHead(complexCSS); + const finalStyleCount = document.head.querySelectorAll("style").length; + + const lastStyle = Array.from( + document.head.querySelectorAll("style"), + ).pop(); + + return { + addedCount: finalStyleCount - initialStyleCount, + styleContent: lastStyle?.textContent, + containsGradient: lastStyle?.textContent.includes("linear-gradient"), + containsMediaQuery: lastStyle?.textContent.includes("@media"), + }; + }); + + expect(result.addedCount).toBe(1); + expect(result.containsGradient).toBe(true); + expect(result.containsMediaQuery).toBe(true); + }); + + test("should handle requireSeries with empty array", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + oc.requireSeries([], (loaded) => { + resolve({ + callbackCalled: true, + loadedArray: loaded, + loadedLength: loaded?.length, + }); + }); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.loadedArray).toEqual([]); + expect(result.loadedLength).toBe(0); + }); + + test("should handle requireSeries with single item", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + window.TestSingleLib = { name: "single", loaded: true }; + + const toLoad = [ + { + global: "TestSingleLib", + url: "data:application/javascript,window.TestSingleLib = window.TestSingleLib || { name: 'single', loaded: true };", + }, + ]; + + oc.requireSeries(toLoad, (loaded) => { + resolve({ + callbackCalled: true, + loadedArray: loaded, + loadedLength: loaded?.length, + firstItem: loaded?.[0], + }); + }); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.loadedLength).toBe(1); + expect(result.firstItem).toEqual({ name: "single", loaded: true }); + }); + + test("should handle requireSeries with multiple items", async ({ page }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + window.TestLib1 = { name: "lib1", version: "1.0" }; + window.TestLib2 = { name: "lib2", version: "2.0" }; + + const toLoad = [ + { + global: "TestLib1", + url: "data:application/javascript,// lib1 loaded", + }, + { + global: "TestLib2", + url: "data:application/javascript,// lib2 loaded", + }, + ]; + + oc.requireSeries(toLoad, (loaded) => { + resolve({ + callbackCalled: true, + loadedArray: loaded, + loadedLength: loaded?.length, + lib1: loaded?.[0], + lib2: loaded?.[1], + }); + }); + }); + }); + + expect(result.callbackCalled).toBe(true); + expect(result.loadedLength).toBe(2); + expect(result.lib1).toEqual({ name: "lib1", version: "1.0" }); + expect(result.lib2).toEqual({ name: "lib2", version: "2.0" }); + }); +});