Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bacpop-79 add timestamp to saved project #35

Merged
merged 19 commits into from
Jul 13, 2023
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: 2 additions & 2 deletions .github/workflows/eslint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [16.x]

steps:
- uses: actions/checkout@v2
Expand All @@ -36,7 +36,7 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [16.x]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/jestCI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [16.x]

steps:
- uses: actions/checkout@v2
Expand All @@ -41,7 +41,7 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [16.x]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions app/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "client",
"name": "Beebop",
"version": "0.1.0",
"private": true,
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions app/client/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const config: PlaywrightTestConfig = {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
screenshot: "only-on-failure",
locale: "en-GB"
},

/* Configure projects for major browsers */
Expand Down
Binary file modified app/client/public/favicon.ico
Binary file not shown.
10 changes: 9 additions & 1 deletion app/client/src/components/SavedProjects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
<div class="container">
<div class="row fw-bold saved-project-headers">
<div class="col-6">Project name</div>
<div class="col-6">Date</div>
</div>
<hr/>
<div v-for="project in savedProjects" :key="project.hash" class="row saved-project-row">
<div class="col-6">
<div class="col-6 saved-project-name">
<button class="clickable brand-text"
@click="loadProject(project)"
@keydown="loadProjectFromKey(project, $event.keyCode)">
{{ project.name }}
</button>
</div>
<div class="col-6 saved-project-date">
{{ displayDateFromTimestamp(project.timestamp) }}
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -41,6 +45,10 @@ function loadProjectFromKey(project: SavedProject, keyCode: number) {
}
}

function displayDateFromTimestamp(timestamp: number) {
return new Date(timestamp).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }).replace(",", "");
}

onMounted(() => {
store.dispatch("getSavedProjects");
});
Expand Down
3 changes: 2 additions & 1 deletion app/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export interface NewProjectRequest {
export interface SavedProject {
id: string,
name: string,
hash: string
hash: string,
timestamp: number
}

export interface ProjectResponse {
Expand Down
5 changes: 4 additions & 1 deletion app/client/tests/e2e/LoggedIn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ test.describe("Logged in Tests", () => {
await expect(page.locator("#cy canvas")).toHaveCount(3);
// can browse back to Home page and see new project in history
await page.goto(config.clientUrl());
await expect(await page.locator(".saved-project-row").last()).toHaveText("test project", { timeout });
await expect(await page.locator(".saved-project-row .saved-project-name").last())
.toHaveText("test project", { timeout });
expect(await (await page.locator(".saved-project-row .saved-project-date").last()).innerText())
.toMatch(/^[0-3][0-9]\/[0-1][0-9]\/20[2-9][0-9] [0-2][0-9]:[0-5][0-9]$/);
const lastProjectIndex = await page.locator(".saved-project-row").count();
// can create a new empty project
await createProject("another test project", page);
Expand Down
21 changes: 16 additions & 5 deletions app/client/tests/unit/components/SavedProjects.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/* eslint-disable import/first */
const { toLocaleString } = Date.prototype;
// eslint-disable-next-line no-extend-native
Date.prototype.toLocaleString = function (locale: any = undefined, ...args: any) {

Check warning on line 4 in app/client/tests/unit/components/SavedProjects.spec.ts

View workflow job for this annotation

GitHub Actions / lint_frontend (16.x)

Unexpected unnamed function
const options = args[0];
return toLocaleString.call(this, "en-GB", { ...options, timeZone: "UTC" });
};

import Vuex from "vuex";
import { RootState } from "@/store/state";
import { shallowMount } from "@vue/test-utils";
Expand All @@ -14,8 +22,8 @@
}));

const savedProjects = [
{ name: "project one", hash: "123abc", id: "ABC-123" },
{ name: "project two", hash: "456def", id: "DEF-123" }
{ name: "project one", hash: "123abc", id: "ABC-123", timestamp: 1687879913811 },
{ name: "project two", hash: "456def", id: "DEF-123", timestamp: 1687879927224 }
];

describe("SavedProjects", () => {
Expand Down Expand Up @@ -47,12 +55,15 @@
const wrapper = getWrapper();
expect(wrapper.find("h4").text()).toBe("Load a previously saved project");
const headers = wrapper.findAll(".saved-project-headers div");
expect(headers.length).toBe(1);
expect(headers.length).toBe(2);
expect(headers.at(0)!.text()).toBe("Project name");
expect(headers.at(1)!.text()).toBe("Date");
const projectRows = wrapper.findAll(".saved-project-row");
expect(projectRows.length).toBe(2);
expect(projectRows.at(0)!.find("div").text()).toBe("project one");
expect(projectRows.at(1)!.find("div").text()).toBe("project two");
expect(projectRows.at(0)!.find(".saved-project-name").text()).toBe("project one");
expect(projectRows.at(0)!.find(".saved-project-date").text()).toBe("27/06/2023 15:31");
expect(projectRows.at(1)!.find(".saved-project-name").text()).toBe("project two");
expect(projectRows.at(1)!.find(".saved-project-date").text()).toBe("27/06/2023 15:32");
});

it("dispatches getSavedProjects on load", () => {
Expand Down
22 changes: 18 additions & 4 deletions app/client/tests/unit/store/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import config from "../../../src/settings/development/config";
import {
mockAxios, mockFailure, mockRootState, mockSuccess
} from "../../mocks";
import mock = jest.mock;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function responseSuccess(data : any) {
Expand Down Expand Up @@ -464,7 +463,12 @@ describe("Actions", () => {
microreact: "waiting"
}
});
const savedProject = { hash: "123", id: "abc", name: "test project" };
const savedProject = {
hash: "123",
id: "abc",
name: "test project",
timestamp: 1687879927224
};
const projectResponse = { test: "value" };
const url = `${serverUrl}/project/abc`;
mockAxios.onGet(url).reply(200, responseSuccess(projectResponse));
Expand Down Expand Up @@ -497,7 +501,12 @@ describe("Actions", () => {
}
});
const dispatch = jest.fn();
const savedProject = { hash: "123", id: "abc", name: "test project" };
const savedProject = {
hash: "123",
id: "abc",
name: "test project",
timestamp: 1687879927224
};
const projectResponse = {
status: {
assign: "finished",
Expand All @@ -516,7 +525,12 @@ describe("Actions", () => {
const commit = jest.fn();
const dispatch = jest.fn();
const state = mockRootState();
const savedProject = { hash: "123", id: "abc", name: "test project" };
const savedProject = {
hash: "123",
id: "abc",
name: "test project",
timestamp: 1687879927224
};
const projectResponse = { test: "value" };
const url = `${serverUrl}/project/abc`;
mockAxios.onGet(url).reply(500, responseError({ error: "test error" }));
Expand Down
5 changes: 4 additions & 1 deletion app/client/tests/unit/store/mutations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ describe("mutations", () => {
});
it("sets saved projects", () => {
const state = mockRootState();
const projects = [{ hash: "123", name: "proj 1", id: "abc" }, { hash: "456", name: "proj 2", id: "def" }];
const projects = [
{ hash: "123", name: "proj 1", id: "abc", timestamp: 1687879925330 },
{ hash: "456", name: "proj 2", id: "def", timestamp: 1687879927224 }
];
mutations.setSavedProjects(state, projects);
expect(state.savedProjects).toBe(projects);
});
Expand Down
6 changes: 4 additions & 2 deletions app/server/src/db/userStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class UserStore {
const projectId = this._newProjectId();
await this._redis.lpush(this._userProjectsKey(user), projectId);
await this._redis.hset(this._projectKey(projectId), "name", projectName);
await this._redis.hset(this._projectKey(projectId), "timestamp", Date.now());
return projectId;
}

Expand All @@ -46,11 +47,12 @@ export class UserStore {

const result = [];
await Promise.all(projectIds.map(async (projectId: string) => {
const values = await this._redis.hmget(this._projectKey(projectId), "name", "hash");
const values = await this._redis.hmget(this._projectKey(projectId), "name", "hash", "timestamp");
result.push({
id: projectId,
name: values[0],
hash: values[1]
hash: values[1],
timestamp: parseInt(values[2])
});
}));

Expand Down
20 changes: 13 additions & 7 deletions app/server/tests/integration/persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,25 @@ describe("User persistence", () => {
connectionCookie = response.headers["set-cookie"][0];
});

const expectTimestampIsSoonAfter = (timestamp: number, now: number) => {
expect(timestamp).toBeGreaterThanOrEqual(now);
expect(timestamp).toBeLessThan(now + 1000);
}

it("adds user project to redis", async () => {
const payload = {
name: "test name"
};
const now = Date.now();
await post("project", payload, connectionCookie);
const userProjects = await getRedisList("beebop:userprojects:mock:1234")
expect(userProjects.length).toBe(1);
const projectId = userProjects[0];
expect(projectId.length).toBe(32);
const projectDetails = await getRedisHash(`beebop:project:${projectId}`);
expect(projectDetails).toStrictEqual({
name: "test name"
});
expect(projectDetails.name).toStrictEqual("test name");
const timestamp = parseInt(projectDetails.timestamp);
expectTimestampIsSoonAfter(timestamp, now);
});

it("adds project's hash to redis", async () => {
Expand All @@ -48,16 +54,16 @@ describe("User persistence", () => {

it("gets user project details from redis", async () => {
await saveRedisList("beebop:userprojects:mock:1234", ["abcd", "efgh"]);
await saveRedisHash("beebop:project:abcd", {name: "test save 1"});
await saveRedisHash("beebop:project:efgh", {name: "test save 2", hash: "1234"});
await saveRedisHash("beebop:project:abcd", {name: "test save 1", timestamp: "1689070004473"});
await saveRedisHash("beebop:project:efgh", {name: "test save 2", hash: "1234", timestamp: "1689070004573"});

const response = await get("projects", connectionCookie);
expect(response.status).toBe(200);
expect(response.data).toStrictEqual({
status: "success",
data: [
{id: "abcd", name: "test save 1", hash: null},
{id: "efgh", name: "test save 2", hash: "1234"}
{id: "abcd", name: "test save 1", hash: null, timestamp: 1689070004473},
{id: "efgh", name: "test save 2", hash: "1234", timestamp: 1689070004573}
],
errors: []
});
Expand Down
21 changes: 14 additions & 7 deletions app/server/tests/unit/db/userStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe("UserStore", () => {
sadd: jest.fn(),
lrange: jest.fn().mockImplementation(() => ["123", "456"]),
hmget: jest.fn().mockImplementation((key: string, ...valueNames: string[]) => {
return valueNames.map((valueName) => `${valueName} for ${key}`);
return valueNames.map((valueName) => valueName === "timestamp" ? 1687879913811 : `${valueName} for ${key}`);
})
} as any;

Expand All @@ -18,10 +18,18 @@ describe("UserStore", () => {
}
} as any;

beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date(1687879913811));
});

beforeEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
jest.useRealTimers();
});

it("saves new project data", async () => {
const sut = new UserStore(mockRedis);
await sut.saveNewProject(mockRequest, "test project name");
Expand All @@ -30,10 +38,9 @@ describe("UserStore", () => {
const projectId = mockRedis.lpush.mock.calls[0][1];
expect(projectId.length).toBe(32);

expect(mockRedis.hset).toHaveBeenCalledTimes(1);
expect(mockRedis.hset.mock.calls[0][0]).toBe(`beebop:project:${projectId}`);
expect(mockRedis.hset.mock.calls[0][1]).toBe("name");
expect(mockRedis.hset.mock.calls[0][2]).toBe("test project name");
expect(mockRedis.hset).toHaveBeenCalledTimes(2);
expect(mockRedis.hset).toHaveBeenNthCalledWith(1, `beebop:project:${projectId}`, "name", "test project name");
expect(mockRedis.hset).toHaveBeenNthCalledWith(2, `beebop:project:${projectId}`, "timestamp", 1687879913811);
});

it("saves project hash", async () => {
Expand All @@ -49,8 +56,8 @@ describe("UserStore", () => {
const sut = new UserStore(mockRedis);
const result = await sut.getUserProjects(mockRequest);
expect(result).toStrictEqual([
{id: "123", name: "name for beebop:project:123", hash: "hash for beebop:project:123"},
{id: "456", name: "name for beebop:project:456", hash: "hash for beebop:project:456"}
{id: "123", name: "name for beebop:project:123", hash: "hash for beebop:project:123", timestamp: 1687879913811},
{id: "456", name: "name for beebop:project:456", hash: "hash for beebop:project:456", timestamp: 1687879913811}
]);

expect(mockRedis.lrange).toHaveBeenCalledTimes(1);
Expand Down
Loading