Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/nativ/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --passWithNoTests"
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage"
},
"sideEffects": [
"**/*.css"
Expand Down Expand Up @@ -79,6 +80,7 @@
"@testing-library/react": "16.3.2",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitest/coverage-v8": "4.1.12",
"clsx": "2.1.1",
"happy-dom": "21.0.4",
"motion": "12.35.0",
Expand Down
33 changes: 33 additions & 0 deletions packages/nativ/src/routes/nativ-routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest"
import { index, rootRoute, route } from "#nativ/routes/nativ-routes"

describe("rootRoute", () => {
it("wires the generated root file when given only children", () => {
const tree = rootRoute([index("pages/home.tsx")])

expect(tree).toEqual({
type: "root",
file: "layouts/__root.gen.tsx",
children: [{ type: "index", file: "pages/home.tsx" }],
})
})

it("wires the generated root file when given nothing", () => {
expect(rootRoute()).toEqual({
type: "root",
file: "layouts/__root.gen.tsx",
children: undefined,
})
})

it("keeps an explicit root file so an app can eject", () => {
const tree = rootRoute("layouts/_root.tsx", [
route("/settings", "pages/settings.tsx"),
])

expect(tree.file).toBe("layouts/_root.tsx")
expect(tree.children).toEqual([
{ type: "route", path: "/settings", file: "pages/settings.tsx" },
])
})
})
66 changes: 66 additions & 0 deletions packages/nativ/src/sw/sw.lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { registerServiceWorkerLifecycle } from "#nativ/sw/sw.lifecycle"

type Listener = (event: unknown) => void

//a service worker global with just what the lifecycle helpers touch
function createScope() {
const listeners = new Map<string, Listener[]>()
return {
skipWaiting: vi.fn(async () => {}),
clients: { claim: vi.fn(async () => {}) },
addEventListener(type: string, listener: Listener) {
listeners.set(type, [...(listeners.get(type) ?? []), listener])
},
dispatch(type: string, event: unknown) {
for (const listener of listeners.get(type) ?? []) listener(event)
},
listenerCount(type: string) {
return listeners.get(type)?.length ?? 0
},
}
}

let scope: ReturnType<typeof createScope>

beforeEach(() => {
scope = createScope()
vi.stubGlobal("self", scope)
})

afterEach(() => {
vi.unstubAllGlobals()
})

describe("registerServiceWorkerLifecycle", () => {
it("skips waiting only on a SKIP_WAITING message", () => {
registerServiceWorkerLifecycle()

scope.dispatch("message", { data: { type: "OTHER" } })
scope.dispatch("message", { data: null })
expect(scope.skipWaiting).not.toHaveBeenCalled()

scope.dispatch("message", { data: { type: "SKIP_WAITING" } })
expect(scope.skipWaiting).toHaveBeenCalledOnce()
})

it("claims clients on activate and holds the event until done", () => {
registerServiceWorkerLifecycle()
const waitUntil = vi.fn()

scope.dispatch("activate", { waitUntil })

expect(scope.clients.claim).toHaveBeenCalledOnce()
expect(waitUntil).toHaveBeenCalledWith(expect.any(Promise))
})

it("registers nothing when both behaviours are turned off", () => {
registerServiceWorkerLifecycle({
claimClients: false,
skipWaitingOnMessage: false,
})

expect(scope.listenerCount("message")).toBe(0)
expect(scope.listenerCount("activate")).toBe(0)
})
})
86 changes: 86 additions & 0 deletions packages/nativ/src/sw/sw.matchers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { createStaticAssetMatcher } from "#nativ/sw/sw.matchers"

const ORIGIN = "https://app.test"

function assetRequest(
path: string,
request: Partial<Pick<Request, "method" | "mode" | "destination">> = {},
) {
return [
new URL(path, ORIGIN),
{
method: "GET",
mode: "cors",
destination: "",
...request,
} as Request,
] as const
}

beforeEach(() => {
vi.stubGlobal("self", { location: { origin: ORIGIN } })
})

afterEach(() => {
vi.unstubAllGlobals()
})

describe("createStaticAssetMatcher", () => {
it("matches same origin scripts, styles, fonts and images", () => {
const matches = createStaticAssetMatcher()

for (const destination of [
"script",
"style",
"font",
"image",
] as const) {
expect(matches(...assetRequest("/x", { destination }))).toBe(true)
}
})

it("matches anything under /assets/ whatever its destination", () => {
const matches = createStaticAssetMatcher()

expect(matches(...assetRequest("/assets/board.json"))).toBe(true)
})

it("skips other destinations outside /assets/", () => {
const matches = createStaticAssetMatcher()

expect(matches(...assetRequest("/data.json"))).toBe(false)
})

it("skips other origins, non GET requests and navigations", () => {
const matches = createStaticAssetMatcher()

expect(
matches(new URL("https://cdn.test/a.js"), {
method: "GET",
mode: "cors",
destination: "script",
} as Request),
).toBe(false)
expect(
matches(...assetRequest("/assets/a.js", { method: "POST" })),
).toBe(false)
expect(
matches(...assetRequest("/assets/a.js", { mode: "navigate" })),
).toBe(false)
})

it("skips /api/ and any extra excluded prefix", () => {
const matches = createStaticAssetMatcher({
excludePathPrefixes: ["/assets/private/"],
})

expect(
matches(
...assetRequest("/api/avatar.png", { destination: "image" }),
),
).toBe(false)
expect(matches(...assetRequest("/assets/private/a.js"))).toBe(false)
expect(matches(...assetRequest("/assets/public/a.js"))).toBe(true)
})
})
52 changes: 52 additions & 0 deletions packages/nativ/src/sw/sw.navigation-match.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest"
import { createNavigationRouteMatch } from "#nativ/sw/sw.navigation-match"

function navigationTo(path: string, mode: RequestMode = "navigate") {
return {
url: new URL(path, "https://app.test"),
request: { mode } as Request,
}
}

describe("createNavigationRouteMatch", () => {
it("never matches a request that is not a navigation", () => {
const matches = createNavigationRouteMatch()

expect(matches(navigationTo("/", "cors"))).toBe(false)
})

it("matches every navigation when given no options", () => {
const matches = createNavigationRouteMatch()

expect(matches(navigationTo("/anything/at/all"))).toBe(true)
})

it("matches the prefix itself and paths under it, not lookalikes", () => {
const matches = createNavigationRouteMatch({ pathPrefix: "/products" })

expect(matches(navigationTo("/products"))).toBe(true)
expect(matches(navigationTo("/products/123"))).toBe(true)
expect(matches(navigationTo("/products-sale"))).toBe(false)
expect(matches(navigationTo("/"))).toBe(false)
})

it("matches by pattern before prefix", () => {
const matches = createNavigationRouteMatch({
pathPattern: /^\/game\/\d+$/,
pathPrefix: "/rules",
})

expect(matches(navigationTo("/game/42"))).toBe(true)
expect(matches(navigationTo("/rules"))).toBe(false)
})

it("hands full control to a custom match", () => {
const matches = createNavigationRouteMatch({
match: (url) => url.searchParams.has("cache"),
pathPrefix: "/products",
})

expect(matches(navigationTo("/products"))).toBe(false)
expect(matches(navigationTo("/other?cache=1"))).toBe(true)
})
})
Loading
Loading