Skip to content

Commit

Permalink
Refactor storage tests (#2031)
Browse files Browse the repository at this point in the history
This improves the storage tests in several ways:

* The local storage helper does _not_ wrap the whole test anymore; this
  caused issues because e.g. individual tests would not be singled out
  or skipped with test.only() or test.skip().
* The `after` parameter of the local storage helper can now also be a
  validating function on the storage content, making it possible to
  assert specific properties (for instead assert that an anchor is set
  without need to know the exact timestamp it was last used).
* Some comment about local storage are made a bit more general.
  • Loading branch information
nmattia authored Nov 10, 2023
1 parent b8ba81d commit 6277ef5
Showing 1 changed file with 140 additions and 95 deletions.
235 changes: 140 additions & 95 deletions src/frontend/src/storage/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,158 @@
import { nonNullish } from "@dfinity/utils";
import { vi } from "vitest";
import { getAnchors, MAX_SAVED_ANCHORS, setAnchorUsed } from ".";
import { MAX_SAVED_ANCHORS, getAnchors, setAnchorUsed } from ".";

testStorage("anchors default to nothing", () => {
test("anchors default to nothing", () => {
expect(getAnchors()).toStrictEqual([]);
});

testStorage(
test(
"old userNumber is recovered",
() => {
expect(getAnchors()).toStrictEqual([BigInt(123456)]);
},
{ localStorage: { before: { userNumber: "123456" } } }
withStorage(
() => {
expect(getAnchors()).toStrictEqual([BigInt(123456)]);
},
{ localStorage: { before: { userNumber: "123456" } } }
)
);

testStorage(
"reading old userNumber migrates anchors",
() => {
vi.useFakeTimers().setSystemTime(new Date(20));
getAnchors();
vi.useRealTimers();
},
{
localStorage: {
before: { userNumber: "123456" },
after: {
anchors: JSON.stringify({
"123456": { lastUsedTimestamp: 20 },
}),
userNumber: "123456",
},
test(
"old userNumber is not deleted",
withStorage(
() => {
getAnchors();
},
}
{
localStorage: {
before: { userNumber: "123456" },
after: (storage) => {
expect(storage["userNumber"]).toBe("123456");
},
},
}
)
);

testStorage("one anchor can be stored", () => {
setAnchorUsed(BigInt(10000));
expect(getAnchors()).toStrictEqual([BigInt(10000)]);
});
test(
"reading old userNumber migrates anchors",
withStorage(
() => {
getAnchors();
},
{
localStorage: {
before: { userNumber: "123456" },
after: (storage) => {
const value = storage["anchors"];
expect(value).toBeDefined();
const anchors = JSON.parse(value);
expect(anchors).toBeTypeOf("object");
expect(anchors["123456"]).toBeDefined();
},
},
}
)
);

testStorage("multiple anchors can be stored", () => {
setAnchorUsed(BigInt(10000));
setAnchorUsed(BigInt(10001));
setAnchorUsed(BigInt(10003));
expect(getAnchors()).toContain(BigInt(10000));
expect(getAnchors()).toContain(BigInt(10001));
expect(getAnchors()).toContain(BigInt(10003));
});
test(
"one anchor can be stored",
withStorage(() => {
setAnchorUsed(BigInt(10000));
expect(getAnchors()).toStrictEqual([BigInt(10000)]);
})
);

testStorage("anchors are sorted", () => {
const anchors = [BigInt(10400), BigInt(10001), BigInt(1011003)];
for (const anchor of anchors) {
setAnchorUsed(anchor);
}
anchors.sort();
expect(getAnchors()).toStrictEqual(anchors);
});
test(
"multiple anchors can be stored",
withStorage(() => {
setAnchorUsed(BigInt(10000));
setAnchorUsed(BigInt(10001));
setAnchorUsed(BigInt(10003));
expect(getAnchors()).toContain(BigInt(10000));
expect(getAnchors()).toContain(BigInt(10001));
expect(getAnchors()).toContain(BigInt(10003));
})
);

testStorage("only N anchors are stored", () => {
for (let i = 0; i < MAX_SAVED_ANCHORS + 5; i++) {
setAnchorUsed(BigInt(i));
}
expect(getAnchors().length).toStrictEqual(MAX_SAVED_ANCHORS);
});
test(
"anchors are sorted",
withStorage(() => {
const anchors = [BigInt(10400), BigInt(10001), BigInt(1011003)];
for (const anchor of anchors) {
setAnchorUsed(anchor);
}
anchors.sort();
expect(getAnchors()).toStrictEqual(anchors);
})
);

testStorage("old anchors are dropped", () => {
vi.useFakeTimers().setSystemTime(new Date(0));
setAnchorUsed(BigInt(10000));
vi.useFakeTimers().setSystemTime(new Date(1));
setAnchorUsed(BigInt(203000));
vi.useFakeTimers().setSystemTime(new Date(2));
for (let i = 0; i < MAX_SAVED_ANCHORS; i++) {
setAnchorUsed(BigInt(i));
}
expect(getAnchors()).not.toContain(BigInt(10000));
expect(getAnchors()).not.toContain(BigInt(203000));
vi.useRealTimers();
});
test(
"only N anchors are stored",
withStorage(() => {
for (let i = 0; i < MAX_SAVED_ANCHORS + 5; i++) {
setAnchorUsed(BigInt(i));
}
expect(getAnchors().length).toStrictEqual(MAX_SAVED_ANCHORS);
})
);

testStorage(
"unknown fields are not dropped",
() => {
vi.useFakeTimers().setSystemTime(new Date(20));
test(
"old anchors are dropped",
withStorage(() => {
vi.useFakeTimers().setSystemTime(new Date(0));
setAnchorUsed(BigInt(10000));
vi.useFakeTimers().setSystemTime(new Date(1));
setAnchorUsed(BigInt(203000));
vi.useFakeTimers().setSystemTime(new Date(2));
for (let i = 0; i < MAX_SAVED_ANCHORS; i++) {
setAnchorUsed(BigInt(i));
}
expect(getAnchors()).not.toContain(BigInt(10000));
expect(getAnchors()).not.toContain(BigInt(203000));
vi.useRealTimers();
},
{
localStorage: {
before: {
anchors: JSON.stringify({
"10000": { lastUsedTimestamp: 10, hello: "world" },
}),
},
after: {
anchors: JSON.stringify({
"10000": { lastUsedTimestamp: 20, hello: "world" },
}),
},
})
);

test(
"unknown fields are not dropped",
withStorage(
() => {
vi.useFakeTimers().setSystemTime(new Date(20));
setAnchorUsed(BigInt(10000));
vi.useRealTimers();
},
}
{
localStorage: {
before: {
anchors: JSON.stringify({
"10000": { lastUsedTimestamp: 10, hello: "world" },
}),
},
after: {
anchors: JSON.stringify({
"10000": { lastUsedTimestamp: 20, hello: "world" },
}),
},
},
}
)
);

/** Run a test that makes use of localStorage. Local storage is always cleared after
* the test was run.
* If `before` is specified, local storage is populated with its content before the test is run.
* If `after` is specified, the content of local storage are checked against `after` after the
* test is run and before local storage is cleared.
/** Test storage usage. Storage is cleared after the callback has returned.
* If `before` is specified, storage is populated with its content before the test is run.
* If `after` is specified, the content of storage are checked against `after` after the
* test is run and before storage is cleared.
* If `after` is a function, the function is called with the content of the storage.
*/
function testStorage(
name: string,
function withStorage(
fn: () => void,
opts?: { localStorage?: { before?: LocalStorage; after?: LocalStorage } }
) {
test(name, () => {
opts?: {
localStorage?: {
before?: LocalStorage;
after?: LocalStorage | ((storage: LocalStorage) => void);
};
}
): () => void {
return () => {
localStorage.clear();
const before = opts?.localStorage?.before;
if (nonNullish(before)) {
Expand All @@ -122,12 +162,17 @@ function testStorage(
const after = opts?.localStorage?.after;
if (nonNullish(after)) {
const actual: LocalStorage = readLocalStorage();
const expected: LocalStorage = after;
expect(actual).toStrictEqual(expected);

if (typeof after === "function") {
after(actual);
} else {
const expected: LocalStorage = after;
expect(actual).toStrictEqual(expected);
}
}

localStorage.clear();
});
};
}

/// Type representing the whole localStorage, used for tests.
Expand Down

0 comments on commit 6277ef5

Please sign in to comment.