diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9b47526 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SERVER_PORT= diff --git a/.gitignore b/.gitignore index ef229ed..64b0d86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.env + coverage/ dist/ node_modules/ +test-results/ +test-results.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d920b47..d805965 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,7 @@ We'd love to accept your patches and contributions to this project. Bellow, you - [Building the SDK](#building-the-sdk) - [Tests](#tests) - [Unit Tests](#unit-tests) + - [E2E Tests](#e2e-tests) - [Code Standards](#code-standards) - [Submitting contributions](#submitting-contributions) - [Commit Messages](#commit-messages) @@ -94,6 +95,24 @@ To run the unit tests, use the following command: bun run test ``` +### E2E Tests + +By default, the application is set to serve a web browser in the port `8080` for Playwright. If this port is already being used on your local machine, make sure you change the port in your `.env`.`SERVER_PORT`. +To run the end-to-end tests, make sure you have installed the browsers supported by playwright by running this command: + +```bash +npx playwright install +``` + +Please refer to [Playwright Documentation](https://playwright.dev/docs/browsers) for details. + +Then, also make sure you have the latest bundled files before running it. Use the following commands: + +```bash +bun run build +bun run test:e2e +``` + ## Code Standards We follow the coding standards set by Biome. Ensure your code follows these guidelines before submitting a pull request. You can run the formatter with the following command: @@ -125,6 +144,10 @@ We do conventional commits, so it will fail on checker with capital case after c The SDK uses following configuration files: - `tsconfig.json`: TypeScript configuration. - `tsup.config.ts`: Configuration for the TSUP bundler. +- `playwright.config.ts`: Configuration for E2E tests runner + +We also have the following variables as part of the `.env` file: +- `PLAYWRIGHT_PORT`: Port used to run the local web browser to run Playwright E2E tests. ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. diff --git a/README.md b/README.md index 175d5f4..a3c108c 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ reportEvent(config, event) 400: ```json { - "status": 204, + "status": 400, "statusText": "No Content", "body": { "errCode": "bad_request", diff --git a/bun.lockb b/bun.lockb index 72143bc..500b13a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml index 0c9079a..61328de 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,2 +1,3 @@ [test] coverage = true +root = "./test" diff --git a/e2e/auctions.test.ts b/e2e/auctions.test.ts new file mode 100644 index 0000000..4c55b7d --- /dev/null +++ b/e2e/auctions.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "@playwright/test"; +import { apis, baseURL } from "../src/constants/apis.constant"; +import { playwrightConstants } from "./config"; + +test.describe("Create Auction via Topsort SDK", () => { + test("should create an auction successfully", async ({ page }) => { + const mockAPIResponse = { + results: [ + { + resultType: "listings", + winners: [], + error: false, + }, + { + resultType: "banners", + winners: [], + error: false, + }, + ], + }; + + await page.route(`${baseURL}/${apis.auctions}`, async (route) => { + await route.fulfill({ json: mockAPIResponse }); + }); + + await page.goto(playwrightConstants.host); + const result = await page.evaluate(() => { + const config = { + apiKey: "rando-api-key", + }; + + const auctionDetails = { + auctions: [ + { + type: "listings", + slots: 3, + category: { id: "cat123" }, + geoTargeting: { location: "US" }, + }, + { + type: "banners", + slots: 1, + device: "desktop", + slotId: "slot123", + category: { ids: ["cat1", "cat2"] }, + geoTargeting: { location: "UK" }, + }, + ], + }; + if (typeof window.sdk.createAuction === "undefined") { + throw new Error("Global function `createAuction` is not available."); + } + + return window.sdk.createAuction(config, auctionDetails); + }); + + expect(result).toEqual(mockAPIResponse); + }); + + test("should fail to call with missing apiKey", async ({ page }) => { + const expectedError = { status: 401, statusText: "API Key is required.", body: {} }; + await page.goto(playwrightConstants.host); + const result = await page.evaluate(() => { + const config = { + apiKey: null, + }; + + const auctionDetails = { + auctions: [ + { + type: "listings", + slots: 3, + category: { id: "cat123" }, + geoTargeting: { location: "US" }, + }, + { + type: "banners", + slots: 1, + device: "desktop", + slotId: "slot123", + category: { ids: ["cat1", "cat2"] }, + geoTargeting: { location: "UK" }, + }, + ], + }; + if (typeof window.sdk.createAuction === "undefined") { + throw new Error("Global function `createAuction` is not available."); + } + + return window.sdk.createAuction(config, auctionDetails); + }); + + expect(result).toEqual(expectedError); + }); +}); diff --git a/e2e/config.ts b/e2e/config.ts new file mode 100644 index 0000000..008e530 --- /dev/null +++ b/e2e/config.ts @@ -0,0 +1,3 @@ +export const playwrightConstants = { + host: `http://localhost:${process.env.SERVER_PORT || 8080}/index.html`, +}; diff --git a/e2e/events.test.ts b/e2e/events.test.ts new file mode 100644 index 0000000..57408e5 --- /dev/null +++ b/e2e/events.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from "@playwright/test"; +import { apis, baseURL } from "../src/constants/apis.constant"; +import { playwrightConstants } from "./config"; + +test.describe("Report Events via Topsort SDK", () => { + test("should report an successfully", async ({ page }) => { + const mockAPIResponse = { + ok: true, + }; + + await page.route(`${baseURL}/${apis.events}`, async (route) => { + await route.fulfill({ json: mockAPIResponse }); + }); + + await page.goto(playwrightConstants.host); + const result = await page.evaluate(() => { + const config = { + apiKey: "rando-api-key", + }; + + const event = { + impressions: [ + { + resolvedBidId: + "ChAGaP5D2ex-UKEEBCOHwvDjEhABkF4FDAx0S5mMD2cOG0w9GhABkEnL2CB6qKIoqeItVgA_InsKd2h0dHBzOi8vd3d3LndlYmEuYmUvZnIvcHJvbW8uaHRtbD91dG1fc291cmNlPW15c2hvcGkmdXRtX21lZGl1bT1iYW5uZXJfMTI4MHg0MDAmdXRtX2NvbnRlbnQ9ZGlzcGxheSZ1dG1fY2FtcGFpZ249c29sZGVuEAU", + id: "1720706109.713344-53B92988-7A49-4679-B18E-465943B46149", + occurredAt: "2024-07-11T13:55:09Z", + opaqueUserId: "38e0a5ff-9f8a-4e80-8969-e5e3f01348e8", + placement: { + path: "/categories/sports", + }, + }, + ], + }; + + if (typeof window.sdk.reportEvent === "undefined") { + throw new Error("Global function `reportEvent` is not available."); + } + + return window.sdk.reportEvent(config, event); + }); + + expect(result).toEqual(mockAPIResponse); + }); + + test("should fail to call with missing apiKey", async ({ page }) => { + const expectedError = { status: 401, statusText: "API Key is required.", body: {} }; + await page.goto(playwrightConstants.host); + const result = await page.evaluate(() => { + const config = { + apiKey: null, + }; + + const event = { + impressions: [ + { + resolvedBidId: + "ChAGaP5D2ex-UKEEBCOHwvDjEhABkF4FDAx0S5mMD2cOG0w9GhABkEnL2CB6qKIoqeItVgA_InsKd2h0dHBzOi8vd3d3LndlYmEuYmUvZnIvcHJvbW8uaHRtbD91dG1fc291cmNlPW15c2hvcGkmdXRtX21lZGl1bT1iYW5uZXJfMTI4MHg0MDAmdXRtX2NvbnRlbnQ9ZGlzcGxheSZ1dG1fY2FtcGFpZ249c29sZGVuEAU", + id: "1720706109.713344-53B92988-7A49-4679-B18E-465943B46149", + occurredAt: "2024-07-11T13:55:09Z", + opaqueUserId: "38e0a5ff-9f8a-4e80-8969-e5e3f01348e8", + placement: { + path: "/categories/sports", + }, + }, + ], + }; + if (typeof window.sdk.reportEvent === "undefined") { + throw new Error("Global function `reportEvent` is not available."); + } + + return window.sdk.reportEvent(config, event); + }); + + expect(result).toEqual(expectedError); + }); +}); diff --git a/e2e/public/index.html b/e2e/public/index.html new file mode 100644 index 0000000..0165b8e --- /dev/null +++ b/e2e/public/index.html @@ -0,0 +1,30 @@ + + + + + + Topsort SDK Test Application + + + +

Test Topsort.js Integration

+ + + diff --git a/e2e/server.ts b/e2e/server.ts new file mode 100644 index 0000000..d9a8a45 --- /dev/null +++ b/e2e/server.ts @@ -0,0 +1,22 @@ +import { file } from "bun"; + +const PORT = process.env.SERVER_PORT || 8080; + +Bun.serve({ + fetch(req) { + const url = new URL(req.url); + const pathname = url.pathname === "/" ? "/index.html" : url.pathname; + const filePath = `./dist${pathname}`; + + try { + if (filePath.endsWith(".ico")) { + return new Response("", { status: 204 }); + } + + return new Response(file(filePath)); + } catch (e) { + return new Response("Not Found", { status: 404 }); + } + }, + port: PORT, +}); diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..f9bc02d --- /dev/null +++ b/global.d.ts @@ -0,0 +1,6 @@ +interface Window { + sdk: { + createAuction: (a, b) => unknown; + reportEvent: (a, b) => unknown; + }; +} diff --git a/package.json b/package.json index 654100a..e017911 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,15 @@ ], "scripts": { "build": "tsup", + "test:e2e": "playwright test", "format": "biome check", "format:fix": "biome check --write", - "prepare": "lefthook install" + "prepare": "lefthook install", + "serve:e2e": "bun run ./e2e/server.ts" }, "devDependencies": { "@biomejs/biome": "1.8.3", + "@playwright/test": "^1.45.2", "@types/bun": "1.1.6", "lefthook": "1.7.5", "msw": "2.3.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b7af23b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + timeout: 30000, + retries: 0, + reporter: [["list"], ["json", { outputFile: "test-results.json" }]], + use: { + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + webServer: { + command: "bun run serve:e2e", + reuseExistingServer: !process.env.CI, + stdout: "ignore", + stderr: "pipe", + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 4eb4110..531c5f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "allowJs": true, "types": ["node"] }, - "include": ["src/*.ts", "src/**/*.ts"], + "include": ["src/*.ts", "src/**/*.ts", "e2e/server.ts"], "exclude": ["node_modules"] } diff --git a/tsup.config.ts b/tsup.config.ts index 0e3f406..49225ea 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -2,11 +2,13 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], - format: ["cjs", "esm"], + format: ["cjs", "esm", "iife"], dts: true, clean: true, minify: true, esbuildOptions(options) { options.keepNames = true; + options.globalName = "Topsort"; }, + onSuccess: "cp -r ./e2e/public/* dist", });