test: setup E2E Tests (#161)
* test: intialize testing environment * test: add an example test for reference * test: add simple navigation test * some initial tests * some changes * name and other small changes * permanently delete a note * permanenlt delete a note * test: improve test readability I have added different id builders for building test ids. They make the tests more readable and fluent. * test lock a note * test add a note to notebook * test favorite a note * test pin a note * test: further improve test readability basically I refactored some frequently performed actions into helper functions * test: check for presence of toast * test: properly test pinned note * test: increase tests reliability * test: fix all tests * perf: load 2co script & fonts when needed * ci: initialize e2e gh test runner * ci: do not run npm ci * test: fix lock note test for all browsers * ci: fix playwright tests * ci: fix yaml syntax error * ci: no need to use custom ssh-agent action for eslint * test: improve lock a note test * ci: add GH_DEPLOY_KEY env in eslint.yml * test: check for state: "visible" in isPresent * test: do not check for toast in lock a note test * test: log crypto error to console * test: skip "lock a note" test for now until further investigation * ci: only run tests on firefox & chromium * fix: fix useMediaQuery for WebKit browsers * ci: try webkit once again * properties tests * test tag a color /properties * test: run some tests sequentially and independently * test: reenable all tests * fix: user only able to type on character in title box * test: skip lock/unlock tests in CI * test edit a notebook * test: fix all tests * test: fix and add more notebook tests * test: do not only run edit topics test * test: make sure all notes tests pass * test: skip add note to notebook tests for now * test: make sure all tests pass Co-authored-by: alihamuh <alihamuh@gmail.com>
24
apps/web/.github/workflows/e2e.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on: [pull_request, push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GH_DEPLOY_KEY: ${{ secrets.GH_DEPLOY_KEY }}
|
||||||
|
CI: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🛎️
|
||||||
|
uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Use Node.js 12.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- run: npm install
|
||||||
|
- uses: microsoft/playwright-github-action@v1
|
||||||
|
- name: Running tests
|
||||||
|
run: npm test
|
||||||
8
apps/web/.github/workflows/eslint.yml
vendored
@@ -14,14 +14,12 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
# The type of runner that the job will run on
|
# The type of runner that the job will run on
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GH_DEPLOY_KEY: ${{ secrets.GH_DEPLOY_KEY }}
|
||||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
steps:
|
steps:
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Setup SSH
|
|
||||||
uses: webfactory/ssh-agent@v0.2.0
|
|
||||||
with:
|
|
||||||
ssh-private-key: ${{ secrets.GH_SSH_KEY }}
|
|
||||||
- name: Use Node.js 12.x
|
- name: Use Node.js 12.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@@ -32,4 +30,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
reporter: github-pr-review
|
reporter: github-pr-review
|
||||||
level: 'warning'
|
level: "warning"
|
||||||
|
|||||||
4
apps/web/.gitignore
vendored
@@ -24,4 +24,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
.now
|
.now
|
||||||
|
|
||||||
public/sodium.js
|
public/sodium.js
|
||||||
|
|
||||||
|
__diff_output__
|
||||||
|
After Width: | Height: | Size: 510 B |
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 337 B |
|
After Width: | Height: | Size: 623 B |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 331 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
37
apps/web/__e2e__/navigation.test.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
const { getTestId } = require("./utils");
|
||||||
|
const { toMatchImageSnapshot } = require("jest-image-snapshot");
|
||||||
|
|
||||||
|
expect.extend({ toMatchImageSnapshot });
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
});
|
||||||
|
|
||||||
|
function createRoute(key, header) {
|
||||||
|
return { buttonId: `navitem-${key}`, header };
|
||||||
|
}
|
||||||
|
const routes = [
|
||||||
|
createRoute("home", "Home"),
|
||||||
|
createRoute("notebooks", "Notebooks"),
|
||||||
|
createRoute("favorites", "Favorites"),
|
||||||
|
createRoute("tags", "Tags"),
|
||||||
|
createRoute("trash", "Trash"),
|
||||||
|
createRoute("settings", "Settings"),
|
||||||
|
].map((route) => [route.header, route]);
|
||||||
|
|
||||||
|
test.each(routes)("navigating to %s", async (_header, route) => {
|
||||||
|
await page.waitForSelector(getTestId(route.buttonId), {
|
||||||
|
state: "visible",
|
||||||
|
});
|
||||||
|
await page.click(getTestId(route.buttonId));
|
||||||
|
await expect(page.textContent(getTestId("routeHeader"))).resolves.toBe(
|
||||||
|
route.header
|
||||||
|
);
|
||||||
|
const navItem = await page.$(getTestId(route.buttonId));
|
||||||
|
await expect(navItem.screenshot()).resolves.toMatchImageSnapshot({
|
||||||
|
failureThreshold: 5,
|
||||||
|
failureThresholdType: "percent",
|
||||||
|
});
|
||||||
|
});
|
||||||
145
apps/web/__e2e__/notebooks.test.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
const { getTestId, createNote, NOTE, NOTEBOOK } = require("./utils");
|
||||||
|
const {
|
||||||
|
navigateTo,
|
||||||
|
openContextMenu,
|
||||||
|
useContextMenu,
|
||||||
|
clickMenuItem,
|
||||||
|
} = require("./utils/actions");
|
||||||
|
const List = require("./utils/listitemidbuilder");
|
||||||
|
const Menu = require("./utils/menuitemidbuilder");
|
||||||
|
const { checkNotePresence } = require("./utils/conditions");
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
}, 600000);
|
||||||
|
|
||||||
|
afterEach(async () => page.close());
|
||||||
|
|
||||||
|
async function fillNotebookDialog(notebook) {
|
||||||
|
await page.fill(getTestId("dialog-nb-name"), notebook.title);
|
||||||
|
|
||||||
|
await page.fill(getTestId("dialog-nb-description"), notebook.description);
|
||||||
|
|
||||||
|
for (let i = 0; i < notebook.topics.length; ++i) {
|
||||||
|
let topic = notebook.topics[i];
|
||||||
|
|
||||||
|
await page.fill(getTestId(`dialog-topic-name-${i}`), topic);
|
||||||
|
|
||||||
|
if (!(await page.$(getTestId(`dialog-topic-name-${i + 1}`))))
|
||||||
|
await page.click(getTestId("dialog-add-topic"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.click(getTestId("dialog-yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNotebook(notebook) {
|
||||||
|
await page.click(getTestId("notebooks-action-button"));
|
||||||
|
|
||||||
|
await fillNotebookDialog(notebook);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNoteAndCheckPresence(note = NOTE) {
|
||||||
|
await createNote(note, "notes");
|
||||||
|
|
||||||
|
// make sure the note has saved.
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
return await checkNotePresence(0, false, note);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNotebookPresence(notebook) {
|
||||||
|
const notebookIdBuilder = List.new("notebook").atIndex(0);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.textContent(notebookIdBuilder.title().build())
|
||||||
|
).resolves.toBe(notebook.title);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.textContent(notebookIdBuilder.body().build())
|
||||||
|
).resolves.toBe(notebook.description);
|
||||||
|
|
||||||
|
await page.click(List.new("notebook").atIndex(0).title().build());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.textContent(List.new("topic").atIndex(0).title().build())
|
||||||
|
).resolves.toBe("General");
|
||||||
|
|
||||||
|
for (let i = 0; i < notebook.topics.length; ++i) {
|
||||||
|
let topic = notebook.topics[i];
|
||||||
|
await expect(
|
||||||
|
page.textContent(
|
||||||
|
List.new("topic")
|
||||||
|
.atIndex(i + 1)
|
||||||
|
.title()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
).resolves.toBe(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.click(getTestId("go-back"));
|
||||||
|
|
||||||
|
return notebookIdBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNotebookAndCheckPresence(notebook = NOTEBOOK) {
|
||||||
|
await navigateTo("notebooks");
|
||||||
|
|
||||||
|
await createNotebook(notebook);
|
||||||
|
|
||||||
|
return await checkNotebookPresence(notebook);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("create a notebook", createNotebookAndCheckPresence);
|
||||||
|
|
||||||
|
test("create a note inside a notebook", async () => {
|
||||||
|
const notebookSelector = await createNotebookAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(notebookSelector);
|
||||||
|
|
||||||
|
await page.click(List.new("topic").atIndex(1).build());
|
||||||
|
|
||||||
|
await createNoteAndCheckPresence();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("edit a notebook", async () => {
|
||||||
|
const notebookSelector = await createNotebookAndCheckPresence();
|
||||||
|
|
||||||
|
await useContextMenu(notebookSelector, () => clickMenuItem("edit"));
|
||||||
|
|
||||||
|
const editedNotebook = {
|
||||||
|
title: "An Edited Notebook",
|
||||||
|
description: "A new edited description",
|
||||||
|
topics: ["Topic 1", "Topic 2", "Topic 3", "Topic 4", "Topic 5"],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fillNotebookDialog(editedNotebook);
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await checkNotebookPresence(editedNotebook);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("edit topics individually", async () => {
|
||||||
|
const notebookSelector = await createNotebookAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(notebookSelector);
|
||||||
|
|
||||||
|
for (let index = 1; index < 4; index++) {
|
||||||
|
await openContextMenu(List.new("topic").atIndex(index).build());
|
||||||
|
|
||||||
|
await page.click(Menu.new("menuitem").item("edit").build());
|
||||||
|
|
||||||
|
const editedTopicTitle = "Topic " + index + " edit 1";
|
||||||
|
await page.fill(getTestId("edit-topic-dialog"), editedTopicTitle);
|
||||||
|
|
||||||
|
await page.click(getTestId("dialog-yes"));
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.textContent(List.new("topic").atIndex(index).title().build())
|
||||||
|
).resolves.toBe(editedTopicTitle);
|
||||||
|
}
|
||||||
|
});
|
||||||
454
apps/web/__e2e__/notes.test.js
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: We are still not checking if toast appears on delete/restore or not.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { getTestId, createNote, NOTE } = require("./utils");
|
||||||
|
const { toMatchImageSnapshot } = require("jest-image-snapshot");
|
||||||
|
expect.extend({ toMatchImageSnapshot });
|
||||||
|
const {
|
||||||
|
navigateTo,
|
||||||
|
clickMenuItem,
|
||||||
|
openContextMenu,
|
||||||
|
confirmDialog,
|
||||||
|
closeContextMenu,
|
||||||
|
useContextMenu,
|
||||||
|
} = require("./utils/actions");
|
||||||
|
const {
|
||||||
|
isToastPresent,
|
||||||
|
isPresent,
|
||||||
|
isAbsent,
|
||||||
|
checkNotePresence,
|
||||||
|
} = require("./utils/conditions");
|
||||||
|
const List = require("./utils/listitemidbuilder");
|
||||||
|
const Menu = require("./utils/menuitemidbuilder");
|
||||||
|
|
||||||
|
const testCISkip = process.env.CI ? test.skip : test;
|
||||||
|
|
||||||
|
var createNoteAndCheckPresence = async function createNoteAndCheckPresence(
|
||||||
|
note = NOTE
|
||||||
|
) {
|
||||||
|
await createNote(note, "home");
|
||||||
|
|
||||||
|
// make sure the note has saved.
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
let noteSelector = await checkNotePresence();
|
||||||
|
|
||||||
|
await page.click(noteSelector, { button: "left" });
|
||||||
|
|
||||||
|
return noteSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
const staticCreateNoteAndCheckPresence = createNoteAndCheckPresence.bind(this);
|
||||||
|
|
||||||
|
async function deleteNoteAndCheckAbsence() {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
await clickMenuItem("movetotrash");
|
||||||
|
|
||||||
|
await confirmDialog();
|
||||||
|
|
||||||
|
await expect(isToastPresent()).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await navigateTo("trash");
|
||||||
|
|
||||||
|
const trashItemSelector = List.new("trash").atIndex(0).title().build();
|
||||||
|
|
||||||
|
await expect(isPresent(trashItemSelector)).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await expect(page.innerText(trashItemSelector)).resolves.toBe(NOTE.title);
|
||||||
|
|
||||||
|
return trashItemSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lockUnlockNote(noteSelector, type) {
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
await clickMenuItem(type);
|
||||||
|
|
||||||
|
await page.fill(getTestId("dialog-vault-pass"), "123abc123abc");
|
||||||
|
|
||||||
|
await confirmDialog();
|
||||||
|
|
||||||
|
await expect(isToastPresent()).resolves.toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNotePinned(noteSelector) {
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
const unpinSelector = Menu.new("menuitem").item("unpin").build();
|
||||||
|
await expect(isPresent(unpinSelector)).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await closeContextMenu(noteSelector);
|
||||||
|
|
||||||
|
// wait for the menu to properly close
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
const note = await page.$(List.new("note").grouped().atIndex(0).build());
|
||||||
|
await expect(note.screenshot()).resolves.toMatchImageSnapshot({
|
||||||
|
failureThreshold: 5,
|
||||||
|
failureThresholdType: "percent",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNoteLocked(noteSelector) {
|
||||||
|
await expect(
|
||||||
|
isPresent(List.new("note").grouped().atIndex(0).locked().build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.textContent(List.new("note").grouped().atIndex(0).body().build())
|
||||||
|
).resolves.toBe("");
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, () =>
|
||||||
|
expect(
|
||||||
|
isPresent(Menu.new("menuitem").item("unlock").build())
|
||||||
|
).resolves.toBeTruthy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNoteColored(noteSelector) {
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isPresent(Menu.new("menuitem").colorCheck("red").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isPresent(List.new("note").atIndex(0).grouped().color("red").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await page.click(getTestId("navitem-red"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isPresent(List.new("note").atIndex(0).color("red").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addNoteToNotebook() {
|
||||||
|
await page.click(getTestId("add-notebook-dialog"));
|
||||||
|
|
||||||
|
await page.fill(getTestId("notebook-name-dialog"), "test notebook");
|
||||||
|
|
||||||
|
await page.press(getTestId("notebook-name-dialog"), "Enter");
|
||||||
|
|
||||||
|
await page.click(getTestId("test-notebook-dialog"));
|
||||||
|
|
||||||
|
await page.click(getTestId("general-dialog"));
|
||||||
|
|
||||||
|
await confirmDialog();
|
||||||
|
|
||||||
|
await expect(isToastPresent()).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await navigateTo("notebooks");
|
||||||
|
|
||||||
|
await page.click(List.new("notebook").atIndex(0).title().build());
|
||||||
|
|
||||||
|
await page.click(List.new("topic").atIndex(0).title().build());
|
||||||
|
|
||||||
|
await checkNotePresence(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each(["independent", "sequential"])("run tests %sly", (type) => {
|
||||||
|
// clear all browser data after running all tests for a single case
|
||||||
|
// so this will clear all data after running test independently & sequentially.
|
||||||
|
afterAll(async () => {
|
||||||
|
try {
|
||||||
|
await jestPlaywright.resetContext();
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// we only close and open new page when running tests independently
|
||||||
|
// otherwise we simply navigate to home.
|
||||||
|
if (type === "independent") {
|
||||||
|
await page.close();
|
||||||
|
page = await browser.newPage();
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
} else {
|
||||||
|
// only navigate to Home if we are not at home
|
||||||
|
if ((await page.textContent(getTestId("routeHeader"))) !== "Home")
|
||||||
|
await navigateTo("home");
|
||||||
|
}
|
||||||
|
}, 600000);
|
||||||
|
|
||||||
|
// we have to reset the createNoteAndCheckPresence after every test
|
||||||
|
afterEach(() => {
|
||||||
|
if (type === "independent") {
|
||||||
|
createNoteAndCheckPresence = staticCreateNoteAndCheckPresence;
|
||||||
|
} else {
|
||||||
|
createNoteAndCheckPresence = async function () {
|
||||||
|
let noteSelector = List.new("note").atIndex(0).grouped().build();
|
||||||
|
await page.click(noteSelector, { button: "left" });
|
||||||
|
return noteSelector;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create a note", createNoteAndCheckPresence);
|
||||||
|
|
||||||
|
test("delete a note", deleteNoteAndCheckAbsence);
|
||||||
|
|
||||||
|
test("restore a note", async () => {
|
||||||
|
const trashItemSelector =
|
||||||
|
type === "independent"
|
||||||
|
? await deleteNoteAndCheckAbsence()
|
||||||
|
: List.new("trash").atIndex(0).title().build();
|
||||||
|
|
||||||
|
if (type === "sequential") await navigateTo("trash");
|
||||||
|
|
||||||
|
await openContextMenu(trashItemSelector);
|
||||||
|
|
||||||
|
await clickMenuItem("restore");
|
||||||
|
|
||||||
|
await expect(isToastPresent()).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await navigateTo("home");
|
||||||
|
|
||||||
|
await checkNotePresence();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("add a note to notebook", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
await clickMenuItem("addto");
|
||||||
|
|
||||||
|
await addNoteToNotebook();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("favorite a note", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, async () => {
|
||||||
|
await clickMenuItem("favorite");
|
||||||
|
});
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, async () => {
|
||||||
|
await expect(
|
||||||
|
isPresent(Menu.new("menuitem").item("unfavorite").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
await navigateTo("favorites");
|
||||||
|
|
||||||
|
await checkNotePresence(0, false);
|
||||||
|
|
||||||
|
await navigateTo("home");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unfavorite a note", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
if (type === "independent") {
|
||||||
|
await useContextMenu(noteSelector, async () => {
|
||||||
|
await clickMenuItem("favorite");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, async () => {
|
||||||
|
await clickMenuItem("unfavorite");
|
||||||
|
});
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, async () => {
|
||||||
|
await expect(
|
||||||
|
isPresent(Menu.new("menuitem").item("favorite").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("favorite a note from properties", async () => {
|
||||||
|
let noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-favorite"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-close"));
|
||||||
|
|
||||||
|
await navigateTo("favorites");
|
||||||
|
|
||||||
|
noteSelector = await checkNotePresence(0, false);
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, async () => {
|
||||||
|
await expect(
|
||||||
|
isPresent(Menu.new("menuitem").item("unfavorite").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
await navigateTo("home");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("assign a color to a note", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
await page.click(Menu.new("menuitem").color("red").build());
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isPresent(getTestId("properties-red-check"))
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await checkNoteColored(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pin a note", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, () => clickMenuItem("pin"));
|
||||||
|
|
||||||
|
await checkNotePinned(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unpin a note", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
if (type === "independent")
|
||||||
|
await useContextMenu(noteSelector, () => clickMenuItem("pin"));
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, () => clickMenuItem("unpin"));
|
||||||
|
|
||||||
|
await useContextMenu(noteSelector, () =>
|
||||||
|
expect(
|
||||||
|
isPresent(Menu.new("menuitem").item("pin").build())
|
||||||
|
).resolves.toBeTruthy()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pin a note from properties", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-pinned"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-close"));
|
||||||
|
|
||||||
|
await checkNotePinned(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("permanently delete a note", async () => {
|
||||||
|
//editor-title //quill
|
||||||
|
const trashItemSelector = await deleteNoteAndCheckAbsence();
|
||||||
|
|
||||||
|
await openContextMenu(trashItemSelector);
|
||||||
|
|
||||||
|
await clickMenuItem("delete");
|
||||||
|
|
||||||
|
await confirmDialog();
|
||||||
|
|
||||||
|
await expect(isToastPresent()).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await expect(page.$(trashItemSelector)).resolves.toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skip("run tests only independently", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
createNoteAndCheckPresence = staticCreateNoteAndCheckPresence;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
await page.goto("http://localhost:3000/");
|
||||||
|
}, 600000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
testCISkip("lock a note", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await lockUnlockNote(noteSelector, "lock");
|
||||||
|
|
||||||
|
await checkNoteLocked(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
testCISkip("unlock a note permanently", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await lockUnlockNote(noteSelector, "lock");
|
||||||
|
|
||||||
|
await lockUnlockNote(noteSelector, "unlock");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isAbsent(List.new("note").grouped().atIndex(0).locked().build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.textContent(List.new("note").grouped().atIndex(0).body().build())
|
||||||
|
).resolves.toContain(NOTE.content);
|
||||||
|
|
||||||
|
await openContextMenu(noteSelector);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isPresent(Menu.new("menuitem").item("lock").build())
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await closeContextMenu(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
testCISkip("lock a note from properties", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-locked"));
|
||||||
|
|
||||||
|
await page.fill(getTestId("dialog-vault-pass"), "123abc123abc");
|
||||||
|
|
||||||
|
await confirmDialog();
|
||||||
|
|
||||||
|
// TODO fix this: no toast is shown when locking note from properties.
|
||||||
|
//await expect(isToastPresent()).resolves.toBeTruthy();
|
||||||
|
|
||||||
|
await checkNoteLocked(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("add a note to notebook from properties", async () => {
|
||||||
|
await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-add-to-nb"));
|
||||||
|
|
||||||
|
await addNoteToNotebook();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("assign a color to note from properties", async () => {
|
||||||
|
const noteSelector = await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await page.click(getTestId("properties-red"));
|
||||||
|
|
||||||
|
await checkNoteColored(noteSelector);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tag with words from properties", async () => {
|
||||||
|
await createNoteAndCheckPresence();
|
||||||
|
|
||||||
|
await page.click(getTestId("properties"));
|
||||||
|
|
||||||
|
await page.fill(getTestId("properties-tag"), "testtag");
|
||||||
|
|
||||||
|
await page.press(getTestId("properties-tag"), "Enter");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
isPresent(getTestId("properties-tag-testtag"))
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
43
apps/web/__e2e__/utils/actions.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
const { getTestId } = require("./index");
|
||||||
|
const Menu = require("./menuitemidbuilder");
|
||||||
|
|
||||||
|
async function navigateTo(pageId) {
|
||||||
|
await page.click(Menu.new("navitem").item(pageId).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickMenuItem(itemId) {
|
||||||
|
await page.click(Menu.new("menuitem").item(itemId).build(), {
|
||||||
|
button: "left",
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openContextMenu(selector) {
|
||||||
|
await page.click(selector, { button: "right" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeContextMenu() {
|
||||||
|
await page.click("body", { button: "left" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function useContextMenu(selector, action) {
|
||||||
|
await openContextMenu(selector);
|
||||||
|
|
||||||
|
await action();
|
||||||
|
|
||||||
|
await closeContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDialog() {
|
||||||
|
await page.click(getTestId("dialog-yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
navigateTo,
|
||||||
|
clickMenuItem,
|
||||||
|
openContextMenu,
|
||||||
|
confirmDialog,
|
||||||
|
closeContextMenu,
|
||||||
|
useContextMenu,
|
||||||
|
};
|
||||||
40
apps/web/__e2e__/utils/conditions.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
const { getTestId, NOTE } = require(".");
|
||||||
|
const List = require("./listitemidbuilder");
|
||||||
|
|
||||||
|
async function isPresent(selector) {
|
||||||
|
try {
|
||||||
|
await page.waitForSelector(selector, { state: "attached", timeout: 10000 });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isAbsent(selector) {
|
||||||
|
try {
|
||||||
|
await page.waitForSelector(selector, { state: "detached", timeout: 10000 });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isToastPresent() {
|
||||||
|
return isPresent(getTestId("toast"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNotePresence(index = 0, grouped = true, note = NOTE) {
|
||||||
|
let noteSelector = List.new("note").atIndex(index).title();
|
||||||
|
if (grouped) noteSelector = noteSelector.grouped();
|
||||||
|
noteSelector = noteSelector.build();
|
||||||
|
|
||||||
|
await page.waitForSelector(noteSelector);
|
||||||
|
await expect(page.innerText(noteSelector)).resolves.toBe(note.title);
|
||||||
|
return noteSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { isPresent, isAbsent, isToastPresent, checkNotePresence };
|
||||||
31
apps/web/__e2e__/utils/index.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
const NOTEBOOK = {
|
||||||
|
title: "Test notebook 1",
|
||||||
|
description: "This is test notebook 1",
|
||||||
|
topics: ["Topic 1", "Very long topic 2", "Topic 3"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const NOTE = {
|
||||||
|
title: "Test 1",
|
||||||
|
content: "This is " + "Test 1".repeat(10),
|
||||||
|
};
|
||||||
|
|
||||||
|
function getTestId(id) {
|
||||||
|
return `[data-test-id="${id}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNote(note, actionButtonId) {
|
||||||
|
await page.click(getTestId(actionButtonId + "-action-button"));
|
||||||
|
|
||||||
|
await page.fill(getTestId("editor-title"), note.title);
|
||||||
|
|
||||||
|
await page.type("#quill .ql-editor", note.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
NOTE,
|
||||||
|
NOTEBOOK,
|
||||||
|
getTestId,
|
||||||
|
createNote,
|
||||||
|
};
|
||||||
48
apps/web/__e2e__/utils/listitemidbuilder.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const { getTestId } = require(".");
|
||||||
|
|
||||||
|
class ListItemIDBuilder {
|
||||||
|
static new(type) {
|
||||||
|
return new ListItemIDBuilder(type);
|
||||||
|
}
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
atIndex(index) {
|
||||||
|
this.index = index;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
this.suffix = "title";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
color(color) {
|
||||||
|
this.suffix = "colors-" + color;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
locked() {
|
||||||
|
this.suffix = "locked";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
body() {
|
||||||
|
this.suffix = "body";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped() {
|
||||||
|
this.isGrouped = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
if (this.isGrouped) this.index++;
|
||||||
|
return getTestId(
|
||||||
|
`${this.type}-${this.index}${this.suffix ? `-${this.suffix}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = ListItemIDBuilder;
|
||||||
30
apps/web/__e2e__/utils/menuitemidbuilder.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const { getTestId } = require(".");
|
||||||
|
|
||||||
|
class MenuItemIDBuilder {
|
||||||
|
static new(type) {
|
||||||
|
return new MenuItemIDBuilder(type);
|
||||||
|
}
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
item(itemId) {
|
||||||
|
this.itemId = itemId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
colorCheck(color) {
|
||||||
|
this.itemId = "colors-" + color + "-check";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
color(color) {
|
||||||
|
this.itemId = "colors-" + color;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
return getTestId(`${this.type}-${this.itemId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = MenuItemIDBuilder;
|
||||||
15
apps/web/jest-playwright.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const IS_CI = !!process.env.CI;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
launchOptions: {
|
||||||
|
headless: IS_CI,
|
||||||
|
},
|
||||||
|
serverOptions: {
|
||||||
|
command: `yarn debug`,
|
||||||
|
port: 3000,
|
||||||
|
launchTimeout: 10000,
|
||||||
|
debug: true,
|
||||||
|
},
|
||||||
|
browsers: IS_CI ? ["firefox", "chromium", "webkit"] : ["chromium"],
|
||||||
|
devices: [],
|
||||||
|
};
|
||||||
3
apps/web/jest.e2e.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: "jest-playwright-preset",
|
||||||
|
};
|
||||||
@@ -32,24 +32,33 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/hookrouter": "^2.2.3",
|
"@types/hookrouter": "^2.2.3",
|
||||||
|
"@types/jest": "^26.0.14",
|
||||||
|
"@types/jest-image-snapshot": "^4.1.0",
|
||||||
"@types/quill": "^2.0.3",
|
"@types/quill": "^2.0.3",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "8.1.0",
|
"babel-loader": "8.1.0",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-react-app": "^5.2.1",
|
"eslint-config-react-app": "^5.2.1",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
|
"eslint-plugin-jest-playwright": "^0.2.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
"eslint-plugin-react": "^7.19.0",
|
"eslint-plugin-react": "^7.19.0",
|
||||||
"eslint-plugin-react-hooks": "^3.0.0",
|
"eslint-plugin-react-hooks": "^3.0.0",
|
||||||
|
"jest-image-snapshot": "^4.2.0",
|
||||||
|
"jest-playwright-preset": "^1.3.1",
|
||||||
|
"playwright": "^1.4.1",
|
||||||
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"source-map-explorer": "^2.4.2",
|
"source-map-explorer": "^2.4.2",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3",
|
||||||
|
"webpack-bundle-analyzer": "^3.9.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "chmod +x ./scripts/ci.sh && ./scripts/ci.sh",
|
"preinstall": "chmod +x ./scripts/ci.sh && ./scripts/ci.sh",
|
||||||
"start": "react-scripts start",
|
"start": "PORT=3001 react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"debug": "BROWSER=none react-scripts start",
|
"debug": "BROWSER=none react-scripts start",
|
||||||
"test": "react-scripts test",
|
"test": "BROWSER= jest --runInBand -c jest.e2e.config.js",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"sodium": "cd public && wget https://github.com/jedisct1/libsodium.js/raw/master/dist/browsers/sodium.js && cd ..",
|
"sodium": "cd public && wget https://github.com/jedisct1/libsodium.js/raw/master/dist/browsers/sodium.js && cd ..",
|
||||||
"update": "npm i @streetwriters/editor@latest @streetwriters/notesnook-core@latest @streetwriters/theme@latest"
|
"update": "npm i @streetwriters/editor@latest @streetwriters/notesnook-core@latest @streetwriters/theme@latest"
|
||||||
@@ -72,4 +81,4 @@
|
|||||||
"last 4 edge version"
|
"last 4 edge version"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,34 +58,11 @@
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
<script>
|
|
||||||
(function (document, src, libName, config) {
|
|
||||||
var script = document.createElement("script");
|
|
||||||
script.src = src;
|
|
||||||
script.async = true;
|
|
||||||
var firstScriptElement = document.getElementsByTagName("script")[0];
|
|
||||||
script.onload = function () {
|
|
||||||
for (var namespace in config) {
|
|
||||||
if (config.hasOwnProperty(namespace)) {
|
|
||||||
window[libName].setup.setConfig(namespace, config[namespace]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window[libName].register();
|
|
||||||
};
|
|
||||||
|
|
||||||
firstScriptElement.parentNode.insertBefore(script, firstScriptElement);
|
|
||||||
})(
|
|
||||||
document,
|
|
||||||
"https://secure.avangate.com/checkout/client/twoCoInlineCart.js",
|
|
||||||
"TwoCoInlineCart",
|
|
||||||
{
|
|
||||||
app: { merchant: "250327951921", iframeLoad: "checkout" },
|
|
||||||
cart: {
|
|
||||||
host: "https:\/\/secure.2checkout.com",
|
|
||||||
customization: "inline",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=DM+Serif+Text:ital@0;1&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'; this.onload=null;"
|
||||||
|
/>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
37
apps/web/scripts/analyze.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||||
|
.BundleAnalyzerPlugin;
|
||||||
|
const webpackConfigProd = require("react-scripts/config/webpack.config")(
|
||||||
|
"production"
|
||||||
|
);
|
||||||
|
|
||||||
|
// this one is optional, just for better feedback on build
|
||||||
|
const chalk = require("chalk");
|
||||||
|
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
|
||||||
|
const green = (text) => {
|
||||||
|
return chalk.green.bold(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
// pushing BundleAnalyzerPlugin to plugins array
|
||||||
|
webpackConfigProd.plugins.push(new BundleAnalyzerPlugin());
|
||||||
|
|
||||||
|
// optional - pushing progress-bar plugin for better feedback;
|
||||||
|
// it can and will work without progress-bar,
|
||||||
|
// but during build time you will not see any messages for 10-60 seconds (depends on the size of the project)
|
||||||
|
// and decide that compilation is kind of hang up on you; progress bar shows nice progression of webpack compilation
|
||||||
|
webpackConfigProd.plugins.push(
|
||||||
|
new ProgressBarPlugin({
|
||||||
|
format: `${green("analyzing...")} ${green("[:bar]")}${green(
|
||||||
|
"[:percent]"
|
||||||
|
)}${green("[:elapsed seconds]")} - :msg`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// actually running compilation and waiting for plugin to start explorer
|
||||||
|
webpack(webpackConfigProd, (err, stats) => {
|
||||||
|
if (err || stats.hasErrors()) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -29,11 +29,11 @@ function getNotebookHeight(item) {
|
|||||||
height += SINGLE_LINE_HEIGHT;
|
height += SINGLE_LINE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description.length > 0) {
|
if (description?.length > 0) {
|
||||||
height += SINGLE_LINE_HEIGHT;
|
height += SINGLE_LINE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description.length > 80) {
|
if (description?.length > 80) {
|
||||||
height += SINGLE_LINE_HEIGHT;
|
height += SINGLE_LINE_HEIGHT;
|
||||||
}
|
}
|
||||||
return height * DEFAULT_FONT_SIZE;
|
return height * DEFAULT_FONT_SIZE;
|
||||||
|
|||||||
@@ -1,4 +1,39 @@
|
|||||||
function upgrade(user) {
|
function loadTCheckout(document, src, libName, config) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.src = src;
|
||||||
|
script.async = true;
|
||||||
|
var firstScriptElement = document.getElementsByTagName("script")[0];
|
||||||
|
script.onload = function () {
|
||||||
|
for (var namespace in config) {
|
||||||
|
if (config.hasOwnProperty(namespace)) {
|
||||||
|
window[libName].setup.setConfig(namespace, config[namespace]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window[libName].register();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
firstScriptElement.parentNode.insertBefore(script, firstScriptElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upgrade(user) {
|
||||||
|
if (!("TwoCoInlineCart" in window)) {
|
||||||
|
await loadTCheckout(
|
||||||
|
document,
|
||||||
|
"https://secure.avangate.com/checkout/client/twoCoInlineCart.js",
|
||||||
|
"TwoCoInlineCart",
|
||||||
|
{
|
||||||
|
app: { merchant: "250327951921", iframeLoad: "checkout" },
|
||||||
|
cart: {
|
||||||
|
host: "https://secure.2checkout.com",
|
||||||
|
customization: "inline",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { TwoCoInlineCart: cart } = window;
|
const { TwoCoInlineCart: cart } = window;
|
||||||
if (!cart) return;
|
if (!cart) return;
|
||||||
cart.setup.setMerchant("250327951921"); // your Merchant code
|
cart.setup.setMerchant("250327951921"); // your Merchant code
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ function Button(props) {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
data-test-id={props.testId}
|
||||||
bg="primary"
|
bg="primary"
|
||||||
width={props.width}
|
width={props.width}
|
||||||
py={2}
|
py={2}
|
||||||
|
|||||||
@@ -109,12 +109,14 @@ class AddNotebookDialog extends React.Component {
|
|||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Input
|
<Input
|
||||||
|
data-test-id="dialog-nb-name"
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => (this.title = e.target.value)}
|
onChange={(e) => (this.title = e.target.value)}
|
||||||
placeholder="Enter name"
|
placeholder="Enter name"
|
||||||
defaultValue={this.title}
|
defaultValue={this.title}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
data-test-id="dialog-nb-description"
|
||||||
sx={{ marginTop: 2 }}
|
sx={{ marginTop: 2 }}
|
||||||
onChange={(e) => (this.description = e.target.value)}
|
onChange={(e) => (this.description = e.target.value)}
|
||||||
placeholder="Enter description (optional)"
|
placeholder="Enter description (optional)"
|
||||||
@@ -127,59 +129,65 @@ class AddNotebookDialog extends React.Component {
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.state.topics.map(
|
{this.state.topics.map((value, index) => {
|
||||||
(value, index) =>
|
if (value.title === "General") {
|
||||||
value.title !== "General" && (
|
return null;
|
||||||
<Flex
|
}
|
||||||
key={value.id || value.title}
|
|
||||||
flexDirection="row"
|
return (
|
||||||
sx={{ marginBottom: 1 }}
|
<Flex
|
||||||
|
key={value.id || value.title}
|
||||||
|
flexDirection="row"
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
data-test-id={
|
||||||
|
"dialog-topic-name-" + (props.edit ? index - 1 : index)
|
||||||
|
}
|
||||||
|
ref={(ref) => {
|
||||||
|
this._inputRefs[index] = ref;
|
||||||
|
if (ref) ref.value = value.title; // set default value
|
||||||
|
}}
|
||||||
|
placeholder="Topic name"
|
||||||
|
onFocus={(e) => {
|
||||||
|
this.lastLength = e.nativeEvent.target.value.length;
|
||||||
|
if (this.state.focusedInputIndex === index) return;
|
||||||
|
this.setState({ focusedInputIndex: index });
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.topics[index].title = e.target.value;
|
||||||
|
}}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.nativeEvent.key === "Enter") {
|
||||||
|
this.addTopic(index);
|
||||||
|
} else if (
|
||||||
|
e.nativeEvent.key === "Backspace" &&
|
||||||
|
this.lastLength === 0 &&
|
||||||
|
index > 0
|
||||||
|
) {
|
||||||
|
this.removeTopic(index);
|
||||||
|
}
|
||||||
|
this.lastLength = e.nativeEvent.target.value.length;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<RebassButton
|
||||||
|
variant="tertiary"
|
||||||
|
sx={{ marginLeft: 1 }}
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
onClick={() => this.performActionOnTopic(index)}
|
||||||
>
|
>
|
||||||
<Input
|
<Box height={20}>
|
||||||
ref={(ref) => {
|
{this.state.focusedInputIndex === index ? (
|
||||||
this._inputRefs[index] = ref;
|
<Icon.Plus size={22} data-test-id="dialog-add-topic" />
|
||||||
if (ref) ref.value = value.title; // set default value
|
) : (
|
||||||
}}
|
<Icon.Minus size={22} />
|
||||||
placeholder="Topic name"
|
)}
|
||||||
onFocus={(e) => {
|
</Box>
|
||||||
this.lastLength = e.nativeEvent.target.value.length;
|
</RebassButton>
|
||||||
if (this.state.focusedInputIndex === index) return;
|
</Flex>
|
||||||
this.setState({ focusedInputIndex: index });
|
);
|
||||||
}}
|
})}
|
||||||
onChange={(e) => {
|
|
||||||
this.topics[index].title = e.target.value;
|
|
||||||
}}
|
|
||||||
onKeyUp={(e) => {
|
|
||||||
if (e.nativeEvent.key === "Enter") {
|
|
||||||
this.addTopic(index);
|
|
||||||
} else if (
|
|
||||||
e.nativeEvent.key === "Backspace" &&
|
|
||||||
this.lastLength === 0 &&
|
|
||||||
index > 0
|
|
||||||
) {
|
|
||||||
this.removeTopic(index);
|
|
||||||
}
|
|
||||||
this.lastLength = e.nativeEvent.target.value.length;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RebassButton
|
|
||||||
variant="tertiary"
|
|
||||||
sx={{ marginLeft: 2 }}
|
|
||||||
px={2}
|
|
||||||
py={1}
|
|
||||||
onClick={() => this.performActionOnTopic(index)}
|
|
||||||
>
|
|
||||||
<Box height={20}>
|
|
||||||
{this.state.focusedInputIndex === index ? (
|
|
||||||
<Icon.Plus size={22} />
|
|
||||||
) : (
|
|
||||||
<Icon.Minus size={22} />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</RebassButton>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -195,6 +203,7 @@ export function showEditNoteDialog(notebook) {
|
|||||||
edit={true}
|
edit={true}
|
||||||
onDone={async (nb) => {
|
onDone={async (nb) => {
|
||||||
const topics = qclone(nb.topics);
|
const topics = qclone(nb.topics);
|
||||||
|
console.log(nb, topics);
|
||||||
delete nb.topics;
|
delete nb.topics;
|
||||||
await store.add({ ...notebook, ...nb });
|
await store.add({ ...notebook, ...nb });
|
||||||
await db.notebooks.notebook(notebook.id).topics.add(...topics);
|
await db.notebooks.notebook(notebook.id).topics.add(...topics);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ function Dialog(props) {
|
|||||||
{props.positiveButton && (
|
{props.positiveButton && (
|
||||||
<RebassButton
|
<RebassButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
data-test-id="dialog-yes"
|
||||||
sx={{ opacity: props.positiveButton.disabled ? 0.7 : 1 }}
|
sx={{ opacity: props.positiveButton.disabled ? 0.7 : 1 }}
|
||||||
mx={1}
|
mx={1}
|
||||||
disabled={props.positiveButton.disabled || false}
|
disabled={props.positiveButton.disabled || false}
|
||||||
@@ -81,6 +82,7 @@ function Dialog(props) {
|
|||||||
{props.negativeButton && (
|
{props.negativeButton && (
|
||||||
<RebassButton
|
<RebassButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
data-test-id="dialog-no"
|
||||||
onClick={props.negativeButton.onClick}
|
onClick={props.negativeButton.onClick}
|
||||||
>
|
>
|
||||||
{props.negativeButton.text || "Cancel"}
|
{props.negativeButton.text || "Cancel"}
|
||||||
|
|||||||
@@ -46,12 +46,18 @@ class MoveDialog extends React.Component {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._inputRef.focus();
|
this._inputRef.focus();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
}} sx={{
|
||||||
|
display: type === "notes" ? "none" : "block",
|
||||||
|
":hover": { color: "primary" },
|
||||||
}} */
|
}} */
|
||||||
|
|
||||||
|
data-test-id="add-notebook-dialog"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* <Input
|
{/* <Input
|
||||||
|
data-test-id="notebook-name-dialog"
|
||||||
ref={(ref) => (this._inputRef = ref)}
|
ref={(ref) => (this._inputRef = ref)}
|
||||||
sx={{ display: mode === "write" ? "block" : "none" }}
|
sx={{ display: mode === "write" ? "block" : "none" }}
|
||||||
my={1}
|
my={1}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function PasswordDialog(props) {
|
|||||||
>
|
>
|
||||||
<Box my={1}>
|
<Box my={1}>
|
||||||
<Input
|
<Input
|
||||||
|
data-test-id="dialog-vault-pass"
|
||||||
ref={passwordRef}
|
ref={passwordRef}
|
||||||
autoFocus
|
autoFocus
|
||||||
variant={isWrong ? "error" : "input"}
|
variant={isWrong ? "error" : "input"}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function TopicDialog(props) {
|
|||||||
>
|
>
|
||||||
<Box my={1}>
|
<Box my={1}>
|
||||||
<Input
|
<Input
|
||||||
|
data-test-id="edit-topic-dialog"
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={ref}
|
ref={ref}
|
||||||
placeholder="Topic title"
|
placeholder="Topic title"
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function Header() {
|
|||||||
<TitleBox
|
<TitleBox
|
||||||
shouldFocus={sessionState === SESSION_STATES.new}
|
shouldFocus={sessionState === SESSION_STATES.new}
|
||||||
title={title}
|
title={title}
|
||||||
|
changeInterval={500}
|
||||||
setTitle={(title) =>
|
setTitle={(title) =>
|
||||||
setSession((state) => {
|
setSession((state) => {
|
||||||
state.session.title = title;
|
state.session.title = title;
|
||||||
@@ -93,7 +94,12 @@ function Header() {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{!isFocusMode && (
|
{!isFocusMode && (
|
||||||
<Flex alignItems="center" onClick={() => toggleProperties()} pr={3}>
|
<Flex
|
||||||
|
alignItems="center"
|
||||||
|
onClick={() => toggleProperties()}
|
||||||
|
pr={3}
|
||||||
|
data-test-id="properties"
|
||||||
|
>
|
||||||
<Icon.Properties size={30} />
|
<Icon.Properties size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import "./editor.css";
|
import "./editor.css";
|
||||||
import { Input } from "@rebass/forms";
|
import { Input } from "@rebass/forms";
|
||||||
|
|
||||||
class TitleBox extends React.Component {
|
/* class TitleBox extends React.Component {
|
||||||
inputRef;
|
inputRef;
|
||||||
|
changeTimeout = 0;
|
||||||
|
state = { text: "" };
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
return nextProps.title !== this.props.title || nextProps.shouldFocus;
|
return nextProps.title !== this.props.title || nextProps.shouldFocus;
|
||||||
}
|
}
|
||||||
@@ -15,28 +17,56 @@ class TitleBox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { title, setTitle, sx } = this.props;
|
const { setTitle, sx, changeInterval } = this.props;
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
ref={(ref) => (this.inputRef = ref)}
|
|
||||||
maxLength={120}
|
|
||||||
placeholder="Untitled"
|
|
||||||
fontFamily="heading"
|
|
||||||
fontWeight="heading"
|
|
||||||
fontSize="heading"
|
|
||||||
color="text"
|
|
||||||
px={2}
|
|
||||||
sx={{
|
|
||||||
borderWidth: 0,
|
|
||||||
":focus": { outline: "none" },
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => {
|
|
||||||
setTitle(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
} */
|
||||||
|
/* export default TitleBox; */
|
||||||
|
var changeTimeout;
|
||||||
|
function TitleBox(props) {
|
||||||
|
const { sx, title, setTitle, changeInterval, shouldFocus } = props;
|
||||||
|
const inputRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
clearTimeout(changeTimeout);
|
||||||
|
inputRef.current.value = title;
|
||||||
|
}, [title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldFocus) inputRef.current.focus();
|
||||||
|
}, [shouldFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
autoFocus={shouldFocus}
|
||||||
|
data-test-id="editor-title"
|
||||||
|
maxLength={120}
|
||||||
|
placeholder="Untitled"
|
||||||
|
fontFamily="heading"
|
||||||
|
fontWeight="heading"
|
||||||
|
fontSize="heading"
|
||||||
|
color="text"
|
||||||
|
px={2}
|
||||||
|
sx={{
|
||||||
|
borderWidth: 0,
|
||||||
|
":focus": { outline: "none" },
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
clearTimeout(changeTimeout);
|
||||||
|
changeTimeout = setTimeout(
|
||||||
|
setTitle.bind(this, e.target.value),
|
||||||
|
changeInterval
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export default TitleBox;
|
|
||||||
|
export default React.memo(TitleBox, (prevProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
prevProps.shouldFocus === nextProps.shouldFocus &&
|
||||||
|
prevProps.title === nextProps.title
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function createIcon(name, rotate = false) {
|
|||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
animate={props.animation}
|
animate={props.animation}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
|
data-test-id={props["data-test-id"]}
|
||||||
>
|
>
|
||||||
<Icon name={name} rotate={rotate} {...props} />
|
<Icon name={name} rotate={rotate} {...props} />
|
||||||
</Animated.Box>
|
</Animated.Box>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function ListContainer(props) {
|
|||||||
<>
|
<>
|
||||||
<Search type={props.type} query={props.query} context={context} />
|
<Search type={props.type} query={props.query} context={context} />
|
||||||
<LoginBar />
|
<LoginBar />
|
||||||
<Flex variant="columnFill" mt={2}>
|
<Flex variant="columnFill" mt={2} data-test-id="note-list">
|
||||||
{props.children
|
{props.children
|
||||||
? props.children
|
? props.children
|
||||||
: props.items.length > 0 && (
|
: props.items.length > 0 && (
|
||||||
@@ -88,6 +88,7 @@ function ListContainer(props) {
|
|||||||
)}
|
)}
|
||||||
{props.button && (
|
{props.button && (
|
||||||
<Button
|
<Button
|
||||||
|
testId={`${props.type}-action-button`}
|
||||||
Icon={props.button.icon || Icon.Plus}
|
Icon={props.button.icon || Icon.Plus}
|
||||||
content={props.button.content}
|
content={props.button.content}
|
||||||
onClick={props.button.onClick}
|
onClick={props.button.onClick}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ function ListItem(props) {
|
|||||||
borderBottomColor: "primary",
|
borderBottomColor: "primary",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
data-test-id={`${props.item.type}-${props.index}`}
|
||||||
>
|
>
|
||||||
{props.pinned && (
|
{props.pinned && (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -162,6 +163,7 @@ function ListItem(props) {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
|
data-test-id={`${props.item.type}-${props.index}-title`}
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -178,6 +180,7 @@ function ListItem(props) {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
|
data-test-id={`${props.item.type}-${props.index}-body`}
|
||||||
>
|
>
|
||||||
{props.body}
|
{props.body}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ function Colors(props) {
|
|||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
color={code}
|
color={code}
|
||||||
strokeWidth={0}
|
strokeWidth={0}
|
||||||
|
data-test-id={`menuitem-colors-${label}`}
|
||||||
/>
|
/>
|
||||||
{colors.includes(label) && (
|
{colors.includes(label) && (
|
||||||
<Icon.Checkmark
|
<Icon.Checkmark
|
||||||
|
data-test-id={`menuitem-colors-${label}-check`}
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: "5px",
|
left: "5px",
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ function Menu(props) {
|
|||||||
(item) =>
|
(item) =>
|
||||||
item.visible !== false && (
|
item.visible !== false && (
|
||||||
<Flex
|
<Flex
|
||||||
|
data-test-id={`menuitem-${item.title
|
||||||
|
.split(" ")
|
||||||
|
.join("")
|
||||||
|
.toLowerCase()}`}
|
||||||
key={item.title}
|
key={item.title}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ function NavigationItem(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
data-test-id={`navitem-${title.toLowerCase()}`}
|
||||||
variant="icon"
|
variant="icon"
|
||||||
py={3}
|
py={3}
|
||||||
label={title}
|
label={title}
|
||||||
|
|||||||
@@ -146,17 +146,18 @@ function Note(props) {
|
|||||||
info={
|
info={
|
||||||
<Flex flex="1 1 auto" justifyContent="space-between">
|
<Flex flex="1 1 auto" justifyContent="space-between">
|
||||||
<Flex variant="rowCenter">
|
<Flex variant="rowCenter">
|
||||||
{note.colors.map((item, index) => (
|
{note.colors.map((item, colorIndex) => (
|
||||||
<Box
|
<Box
|
||||||
key={item}
|
key={item}
|
||||||
style={{
|
style={{
|
||||||
width: 13,
|
width: 13,
|
||||||
marginLeft: index ? -8 : 0,
|
marginLeft: colorIndex ? -8 : 0,
|
||||||
marginRight: index === note.colors.length - 1 ? 5 : 0,
|
marginRight: colorIndex === note.colors.length - 1 ? 5 : 0,
|
||||||
height: 13,
|
height: 13,
|
||||||
backgroundColor: COLORS[item],
|
backgroundColor: COLORS[item],
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
}}
|
}}
|
||||||
|
data-test-id={`note-${index}-colors-${item}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<TimeAgo datetime={note.dateCreated} />
|
<TimeAgo datetime={note.dateCreated} />
|
||||||
@@ -165,6 +166,7 @@ function Note(props) {
|
|||||||
size={13}
|
size={13}
|
||||||
color={theme.colors.fontTertiary}
|
color={theme.colors.fontTertiary}
|
||||||
sx={{ ml: 1 }}
|
sx={{ ml: 1 }}
|
||||||
|
data-test-id={`note-${index}-locked`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{note.favorite && (
|
{note.favorite && (
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ function Properties() {
|
|||||||
>
|
>
|
||||||
Properties
|
Properties
|
||||||
<Text
|
<Text
|
||||||
|
data-test-id="properties-close"
|
||||||
as="span"
|
as="span"
|
||||||
onClick={() => toggleProperties()}
|
onClick={() => toggleProperties()}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -114,6 +115,7 @@ function Properties() {
|
|||||||
key={tool.key}
|
key={tool.key}
|
||||||
toggleKey={tool.key}
|
toggleKey={tool.key}
|
||||||
onToggle={(state) => changeState(tool.key, state)}
|
onToggle={(state) => changeState(tool.key, state)}
|
||||||
|
testId={`properties-${tool.key}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -122,6 +124,7 @@ function Properties() {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await showMoveNoteDialog([sessionId]);
|
await showMoveNoteDialog([sessionId]);
|
||||||
}}
|
}}
|
||||||
|
data-test-id="properties-add-to-nb"
|
||||||
>
|
>
|
||||||
{notebook ? "Move to another notebook" : "Add to notebook"}
|
{notebook ? "Move to another notebook" : "Add to notebook"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -155,7 +158,7 @@ function Properties() {
|
|||||||
navigate(`/notebooks/${notebookData.id}/${index}`);
|
navigate(`/notebooks/${notebookData.id}/${index}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{notebookData.topic}
|
{notebookData?.topic}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -168,6 +171,7 @@ function Properties() {
|
|||||||
onClick={() => setColor(label)}
|
onClick={() => setColor(label)}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
mt={4}
|
mt={4}
|
||||||
|
data-test-id={`properties-${label}`}
|
||||||
>
|
>
|
||||||
<Flex key={label}>
|
<Flex key={label}>
|
||||||
<Icon.Circle size={24} color={code} />
|
<Icon.Circle size={24} color={code} />
|
||||||
@@ -176,13 +180,18 @@ function Properties() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{colors.includes(label) && (
|
{colors.includes(label) && (
|
||||||
<Icon.Checkmark color="primary" size={20} />
|
<Icon.Checkmark
|
||||||
|
color="primary"
|
||||||
|
size={20}
|
||||||
|
data-test-id={`properties-${label}-check`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
data-test-id="properties-tag"
|
||||||
placeholder="#tag"
|
placeholder="#tag"
|
||||||
mt={4}
|
mt={4}
|
||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
@@ -210,6 +219,7 @@ function Properties() {
|
|||||||
>
|
>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<Text
|
<Text
|
||||||
|
data-test-id={`properties-tag-${tag}`}
|
||||||
key={tag}
|
key={tag}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "primary",
|
backgroundColor: "primary",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function Toggle(props) {
|
|||||||
mr={1}
|
mr={1}
|
||||||
sx={{ borderRadius: "default", cursor: "pointer" }}
|
sx={{ borderRadius: "default", cursor: "pointer" }}
|
||||||
onClick={() => onToggle(!isOn)}
|
onClick={() => onToggle(!isOn)}
|
||||||
|
data-test-id={props.testId}
|
||||||
>
|
>
|
||||||
{isOn ? <icons.on color="primary" /> : <icons.off />}
|
{isOn ? <icons.on color="primary" /> : <icons.off />}
|
||||||
<Text mt={1} variant="body" color={isOn ? "primary" : "text"}>
|
<Text mt={1} variant="body" color={isOn ? "primary" : "text"}>
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ function Header(props) {
|
|||||||
height={38}
|
height={38}
|
||||||
width={38}
|
width={38}
|
||||||
>
|
>
|
||||||
<Icon.ChevronLeft size={38} color="fontPrimary" />
|
<Icon.ChevronLeft
|
||||||
|
size={38}
|
||||||
|
color="fontPrimary"
|
||||||
|
data-test-id="go-back"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Icon.Menu
|
<Icon.Menu
|
||||||
@@ -68,7 +72,16 @@ function Header(props) {
|
|||||||
size={28}
|
size={28}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Heading fontSize="heading" color={"text"}>
|
<Icon.Menu
|
||||||
|
onClick={toggleSideMenu}
|
||||||
|
sx={{
|
||||||
|
ml: 0,
|
||||||
|
mr: 4,
|
||||||
|
display: ["block", "none", "none"],
|
||||||
|
}}
|
||||||
|
size={28}
|
||||||
|
/>
|
||||||
|
<Heading data-test-id="routeHeader" fontSize="heading" color={"text"}>
|
||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import ListItem from "../list-item";
|
import ListItem from "../list-item";
|
||||||
import { confirm } from "../dialogs/confirm";
|
import { confirm } from "../dialogs/confirm";
|
||||||
import * as Icon from "../icons";
|
import * as Icon from "../icons";
|
||||||
import { store } from "../../stores/notebook-store";
|
import { store } from "../../stores/trash-store";
|
||||||
import { Flex, Text } from "rebass";
|
import { Flex, Text } from "rebass";
|
||||||
import TimeAgo from "timeago-react";
|
import TimeAgo from "timeago-react";
|
||||||
import { toTitleCase } from "../../utils/string";
|
import { toTitleCase } from "../../utils/string";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=DM+Serif+Text:ital@0;1&display=swap");
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class CryptoWorker {
|
|||||||
if (_type === type && _mId === messageId) {
|
if (_type === type && _mId === messageId) {
|
||||||
this.worker.removeEventListener("message", onMessage);
|
this.worker.removeEventListener("message", onMessage);
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
|
console.error(data.error);
|
||||||
return reject(data.error);
|
return reject(data.error);
|
||||||
}
|
}
|
||||||
resolve(data);
|
resolve(data);
|
||||||
|
|||||||
@@ -34,8 +34,13 @@ function ToastContainer(props) {
|
|||||||
const { type, message, actions } = props;
|
const { type, message, actions } = props;
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Flex justifyContent="center" alignContent="center">
|
<Flex data-test-id="toast" justifyContent="center" alignContent="center">
|
||||||
<Text variant="body" fontSize="menu" mr={2}>
|
<Text
|
||||||
|
data-test-id="toast-message"
|
||||||
|
variant="body"
|
||||||
|
fontSize="menu"
|
||||||
|
mr={2}
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
{actions?.map((action) => (
|
{actions?.map((action) => (
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ const useMediaQuery = (mediaQuery) => {
|
|||||||
const mediaQueryList = window.matchMedia(mediaQuery);
|
const mediaQueryList = window.matchMedia(mediaQuery);
|
||||||
const documentChangeHandler = () => setIsVerified(!!mediaQueryList.matches);
|
const documentChangeHandler = () => setIsVerified(!!mediaQueryList.matches);
|
||||||
|
|
||||||
mediaQueryList.addEventListener("change", documentChangeHandler);
|
if (mediaQueryList.addEventListener)
|
||||||
|
mediaQueryList.addEventListener("change", documentChangeHandler);
|
||||||
|
else mediaQueryList.addListener(documentChangeHandler);
|
||||||
|
|
||||||
documentChangeHandler();
|
documentChangeHandler();
|
||||||
return () => {
|
return () => {
|
||||||
mediaQueryList.removeEventListener("change", documentChangeHandler);
|
if (mediaQueryList.removeEventListener)
|
||||||
|
mediaQueryList.removeEventListener("change", documentChangeHandler);
|
||||||
|
else mediaQueryList.removeListener(documentChangeHandler);
|
||||||
};
|
};
|
||||||
}, [mediaQuery]);
|
}, [mediaQuery]);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ function Notebooks() {
|
|||||||
/>
|
/>
|
||||||
<AddNotebookDialog
|
<AddNotebookDialog
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
|
edit={false}
|
||||||
onDone={async (nb) => {
|
onDone={async (nb) => {
|
||||||
await add(nb);
|
await add(nb);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||