Skip to content

Commit

Permalink
singleton for more pragmatic usage of opfs, and full test coverage of it
Browse files Browse the repository at this point in the history
  • Loading branch information
Mazuh committed Nov 23, 2023
1 parent c36922e commit 4b7cefb
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 41 deletions.
16 changes: 10 additions & 6 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@ import "@testing-library/jest-dom";
import { act, render, screen } from "@testing-library/react";
import App from "./App";
import {
makeOpfsAdapter,
makeOpfsAdapterSingleton,
isPersistenceSupported,
} from "./services/origin-private-file-system";

jest.mock("./services/origin-private-file-system.ts", () => ({
makeOpfsAdapter: jest.fn(),
makeOpfsAdapterSingleton: jest
.fn()
.mockReturnValue(
jest.fn().mockResolvedValue({ retrieve: jest.fn(), persist: jest.fn() })
),
isPersistenceSupported: jest.fn().mockReturnValue(true),
}));

describe("App", () => {
beforeEach(() => {
jest.useFakeTimers();

(makeOpfsAdapter as jest.Mock).mockClear();
(makeOpfsAdapter as jest.Mock).mockResolvedValue({
(makeOpfsAdapterSingleton as jest.Mock).mockClear();
(makeOpfsAdapterSingleton as jest.Mock).mockReturnValue(async () => ({
retrieve: jest
.fn()
.mockResolvedValue("d48c2b8f-e557-4503-8fac-4561bba582ac"),
persist: jest.fn(),
});
}));
});

it("Renders fine at first", async () => {
Expand All @@ -35,7 +39,7 @@ describe("App", () => {

await act(() => jest.runAllTimers());

(makeOpfsAdapter as jest.Mock).mockResolvedValue({
(makeOpfsAdapterSingleton as jest.Mock).mockResolvedValue({
retrieve: jest
.fn()
.mockResolvedValue("01fb9ee8-926c-483e-9eb5-f020762d4b00"),
Expand Down
16 changes: 2 additions & 14 deletions src/services/opfs-client-session.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import { validate as validateUuid } from "uuid";
import {
OriginPrivateFileSystemAdapter,
makeOpfsAdapter,
} from "./origin-private-file-system";
import { makeOpfsAdapterSingleton } from "./origin-private-file-system";

let opfs: OriginPrivateFileSystemAdapter<string>;
const getOpfsAdapter = async (): Promise<
OriginPrivateFileSystemAdapter<string>
> => {
if (!opfs) {
opfs = await makeOpfsAdapter<string>("client-session.txt");
}

return opfs;
};
const getOpfsAdapter = makeOpfsAdapterSingleton<string>("client-session.txt");

export async function retrieveClientSessionUuid(): Promise<string> {
const opfs = await getOpfsAdapter();
Expand Down
151 changes: 130 additions & 21 deletions src/services/origin-private-file-system.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { makeOpfsAdapter } from "./origin-private-file-system";
import {
isPersistenceSupported,
makeOpfsAdapterSingleton,
} from "./origin-private-file-system";

describe("Origin private file system (OPFS) adapter", () => {
beforeAll(() => {
Object.defineProperty(global.navigator, "storage", {
value: {
getDirectory: jest.fn().mockImplementation(() => ({
getFileHandle: jest.fn().mockImplementation(() => ({
getDirectory: jest.fn().mockResolvedValue({
getFileHandle: jest.fn().mockResolvedValue({
getFile: jest.fn().mockResolvedValue({
text: jest
.fn()
Expand All @@ -17,34 +20,59 @@ describe("Origin private file system (OPFS) adapter", () => {
write: jest.fn(),
close: jest.fn(),
}),
})),
})),
}),
}),
},
writable: true,
});
});

it("Can generate specific singletons (for more pragmatic usage)", async () => {
const getOpfsAdapter = makeOpfsAdapterSingleton<string>(
"something-in-the-way.txt"
);
const opfs1 = await getOpfsAdapter();
const opfs2 = await getOpfsAdapter();
expect(opfs1).toBe(opfs2);
});

it("Necessarily means that two different specific singletons are indeed different references", async () => {
const getOpfsAdapter1 = makeOpfsAdapterSingleton<string>(
"the-man-who-sold-the-world.txt"
);
const opfs1 = await getOpfsAdapter1();

const getOpfsAdapter2 = makeOpfsAdapterSingleton<string>(
"i-never-lost-control.txt"
);
const opfs2 = await getOpfsAdapter2();

expect(opfs1).not.toBe(opfs2);
});

it("Integrates with real OPFS to retrieve parsed JSON content", async () => {
const getFileHandleMock = jest.fn().mockImplementation(() => ({
const getFileHandleMock = jest.fn().mockResolvedValue({
getFile: jest.fn().mockResolvedValue({
text: jest
.fn()
.mockResolvedValue('{ "taste": "pepsicola", "surprise": false }'),
}),
}));
});

Object.defineProperty(global.navigator, "storage", {
value: {
getDirectory: jest.fn().mockImplementation(() => ({
getDirectory: jest.fn().mockResolvedValue({
getFileHandle: getFileHandleMock,
})),
}),
},
writable: true,
});

const opfs = await makeOpfsAdapter<{ taste: string; surprise: boolean }>(
"lana.json"
);
const getOpfsAdapter = makeOpfsAdapterSingleton<{
taste: string;
surprise: boolean;
}>("lana.json");
const opfs = await getOpfsAdapter();
const retrieved = await opfs.retrieve();
expect(retrieved).toEqual({ taste: "pepsicola", surprise: false });

Expand All @@ -53,36 +81,117 @@ describe("Origin private file system (OPFS) adapter", () => {
});
});

it("Retrieves null if there is no content on a given file", async () => {
Object.defineProperty(global.navigator, "storage", {
value: {
getDirectory: jest.fn().mockResolvedValue({
getFileHandle: jest.fn().mockResolvedValue({
getFile: jest.fn().mockResolvedValue({
text: jest.fn().mockResolvedValue(""),
}),
}),
}),
},
writable: true,
});

const getOpfsAdapter = makeOpfsAdapterSingleton<{
taste: string;
surprise: boolean;
}>("lana_banana.json");
const opfs = await getOpfsAdapter();
const retrieved = await opfs.retrieve();
expect(retrieved).toEqual(null);
});

it("Integrates with real OPFS to persist JSON content", async () => {
const writeMock = jest.fn();
const closeMock = jest.fn();
const getFileHandleMock = jest.fn().mockImplementation(() => ({
const getFileHandleMock = jest.fn().mockResolvedValue({
createWritable: jest.fn().mockResolvedValue({
write: writeMock,
close: closeMock,
}),
}));
});

Object.defineProperty(global.navigator, "storage", {
value: {
getDirectory: jest.fn().mockImplementation(() => ({
getDirectory: jest.fn().mockResolvedValue({
getFileHandle: getFileHandleMock,
})),
}),
},
writable: true,
});

const opfs = await makeOpfsAdapter<{ handsOn: string; nameOn: string }>(
"lana.json"
);
const getOpfsAdapter = makeOpfsAdapterSingleton<{
handsOn: string;
nameOn: string;
}>("delrey.json");
const opfs = await getOpfsAdapter();
await opfs.persist({ handsOn: "hips", nameOn: "lips" });

expect(getFileHandleMock).toHaveBeenCalledWith("lana.json", {
expect(getFileHandleMock).toHaveBeenCalledWith("delrey.json", {
create: true,
});
expect(writeMock).toHaveBeenCalledWith(
'{"handsOn":"hips","nameOn":"lips"}'
);
expect(closeMock).toHaveBeenCalled();
expect(closeMock).toHaveBeenCalledTimes(1);
});

it("Closes the writable even if an error is throwed in the writing side effect", async () => {
const writeMock = jest.fn().mockRejectedValue(new Error("Writing error!"));
const closeMock = jest.fn();
const getFileHandleMock = jest.fn().mockResolvedValue({
createWritable: jest.fn().mockResolvedValue({
write: writeMock,
close: closeMock,
}),
});

Object.defineProperty(global.navigator, "storage", {
value: {
getDirectory: jest.fn().mockResolvedValue({
getFileHandle: getFileHandleMock,
}),
},
writable: true,
});

const getOpfsAdapter = makeOpfsAdapterSingleton<{
handsOn: string;
nameOn: string;
}>("delrey.json");
const opfs = await getOpfsAdapter();

await expect(() =>
opfs.persist({ handsOn: "hips", nameOn: "lips" })
).rejects.toEqual(new Error("Writing error!"));

expect(closeMock).toHaveBeenCalledTimes(1);
});

it("Can confirm full support for the offline persistence", () => {
Object.defineProperty(window, "FileSystemFileHandle", {
value: {
prototype: {
createWritable: () => {},
},
},
writable: true,
});

expect(isPersistenceSupported()).toBe(true);
});

it("Can detect lack of full support for the offline persistence: writable async", () => {
Object.defineProperty(window, "FileSystemFileHandle", {
value: {
prototype: {},
},
writable: true,
});

expect(isPersistenceSupported()).toBe(false);
});
});
14 changes: 14 additions & 0 deletions src/services/origin-private-file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ export interface OriginPrivateFileSystemAdapter<T> {
persist: (data: T) => Promise<void>;
}

export function makeOpfsAdapterSingleton<T>(
filename: string
): () => Promise<OriginPrivateFileSystemAdapter<T>> {
let opfsAdapter: OriginPrivateFileSystemAdapter<T>;

return async () => {
if (!opfsAdapter) {
opfsAdapter = await makeOpfsAdapter<T>(filename);
}

return opfsAdapter;
};
}

export async function makeOpfsAdapter<T>(
filename: string
): Promise<OriginPrivateFileSystemAdapter<T>> {
Expand Down

0 comments on commit 4b7cefb

Please sign in to comment.