mirror of https://github.com/TriliumNext/Notes
Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms
commit
c9bf752b1f
@ -1,53 +1,53 @@
|
||||
name: Publish Docker image
|
||||
on:
|
||||
push:
|
||||
tags: [v*]
|
||||
push:
|
||||
tags: [v*]
|
||||
jobs:
|
||||
push_to_registries:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: |
|
||||
zadam/trilium
|
||||
ghcr.io/zadam/trilium
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}-latest
|
||||
type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to GitHub Docker Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create server-package.json
|
||||
run: cat package.json | grep -v electron > server-package.json
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
push: true
|
||||
cache-from: type=registry,ref=zadam/trilium:buildcache
|
||||
cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
push_to_registries:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: |
|
||||
zadam/trilium
|
||||
ghcr.io/zadam/trilium
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}-latest
|
||||
type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
install: true
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to GitHub Docker Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create server-package.json
|
||||
run: cat package.json | grep -v electron > server-package.json
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
push: true
|
||||
cache-from: type=registry,ref=zadam/trilium:buildcache
|
||||
cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"lokalise.i18n-ally",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
"recommendations": ["lokalise.i18n-ally", "editorconfig.editorconfig"]
|
||||
}
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
// nodemon should be installed globally, use npm i -g nodemon
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "nodemon start-server",
|
||||
"program": "${workspaceFolder}/src/www",
|
||||
"request": "launch",
|
||||
"restart": true,
|
||||
"runtimeExecutable": "nodemon",
|
||||
"env": {
|
||||
"TRILIUM_ENV": "dev",
|
||||
"TRILIUM_DATA_DIR": "./data"
|
||||
},
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"outputCapture": "std",
|
||||
},
|
||||
]
|
||||
}
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
// nodemon should be installed globally, use npm i -g nodemon
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "nodemon start-server",
|
||||
"program": "${workspaceFolder}/src/www",
|
||||
"request": "launch",
|
||||
"restart": true,
|
||||
"runtimeExecutable": "nodemon",
|
||||
"env": {
|
||||
"TRILIUM_ENV": "dev",
|
||||
"TRILIUM_DATA_DIR": "./data"
|
||||
},
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "node",
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,27 +1,22 @@
|
||||
{
|
||||
"editor.formatOnSave": false,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"./src/public/translations",
|
||||
"./translations"
|
||||
],
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/nightly.yml"
|
||||
],
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["./src/public/translations", "./translations"],
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [".github/workflows/nightly.yml"],
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,24 @@
|
||||
{
|
||||
// Place your Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
// Place your Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
|
||||
"JQuery HTMLElement field": {
|
||||
"scope": "typescript",
|
||||
"prefix": "jqf",
|
||||
"body": [
|
||||
"private $${1:name}!: JQuery<HTMLElement>;"
|
||||
]
|
||||
}
|
||||
}
|
||||
"JQuery HTMLElement field": {
|
||||
"scope": "typescript",
|
||||
"prefix": "jqf",
|
||||
"body": ["private $${1:name}!: JQuery<HTMLElement>;"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import anonymizationService from '../src/services/anonymization.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import anonymizationService from "../src/services/anonymization.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getFullAnonymizationScript());
|
||||
fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript());
|
||||
|
||||
@ -1,2 +1,148 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
|
||||
/*# sourceMappingURL=normalize.min.css.map */
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
html {
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: underline;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"],
|
||||
button {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner,
|
||||
button::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring,
|
||||
button:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
display: table;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
/*# sourceMappingURL=normalize.min.css.map */
|
||||
|
||||
@ -1,33 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import dumpService from './inc/dump.js';
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import dumpService from "./inc/dump.js";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.command('$0 <path_to_document> <target_directory>', 'dump the contents of document.db into the target directory', (yargs) => {
|
||||
return yargs
|
||||
.option('path_to_document', { alias: 'p', describe: 'path to the document.db', type: 'string', demandOption: true })
|
||||
.option('target_directory', { alias: 't', describe: 'path of the directory into which the notes should be dumped', type: 'string', demandOption: true });
|
||||
}, (argv) => {
|
||||
try {
|
||||
dumpService.dumpDocument(argv.path_to_document, argv.target_directory, {
|
||||
includeDeleted: argv.includeDeleted,
|
||||
password: argv.password
|
||||
});
|
||||
.command(
|
||||
"$0 <path_to_document> <target_directory>",
|
||||
"dump the contents of document.db into the target directory",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("path_to_document", { alias: "p", describe: "path to the document.db", type: "string", demandOption: true })
|
||||
.option("target_directory", { alias: "t", describe: "path of the directory into which the notes should be dumped", type: "string", demandOption: true });
|
||||
},
|
||||
(argv) => {
|
||||
try {
|
||||
dumpService.dumpDocument(argv.path_to_document, argv.target_directory, {
|
||||
includeDeleted: argv.includeDeleted,
|
||||
password: argv.password
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Unrecoverable error:`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Unrecoverable error:`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.option('password', {
|
||||
type: 'string',
|
||||
description: 'Set password to be able to decrypt protected notes.'
|
||||
)
|
||||
.option("password", {
|
||||
type: "string",
|
||||
description: "Set password to be able to decrypt protected notes."
|
||||
})
|
||||
.option('include-deleted', {
|
||||
type: 'boolean',
|
||||
.option("include-deleted", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: 'If set to true, dump also deleted notes.'
|
||||
description: "If set to true, dump also deleted notes."
|
||||
})
|
||||
.parse();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES6",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES6",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import App from "./support/app";
|
||||
|
||||
test("Displays translation on desktop", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
|
||||
await expect(page.locator("#left-pane .quick-search input"))
|
||||
.toHaveAttribute("placeholder", "Quick search");
|
||||
});
|
||||
|
||||
test("Displays translation on mobile", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto({ isMobile: true });
|
||||
|
||||
await expect(page.locator("#mobile-sidebar-wrapper .quick-search input"))
|
||||
.toHaveAttribute("placeholder", "Quick search");
|
||||
});
|
||||
|
||||
test("Displays translations in Settings", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.closeAllTabs();
|
||||
await app.goToSettings();
|
||||
await app.noteTree.getByText("Appearance").click();
|
||||
|
||||
await expect(app.currentNoteSplit).toContainText("Localization");
|
||||
await expect(app.currentNoteSplit).toContainText("Language");
|
||||
});
|
||||
|
||||
test("User can change language from settings", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
|
||||
await app.closeAllTabs();
|
||||
await app.goToSettings();
|
||||
await app.noteTree.getByText("Appearance").click();
|
||||
|
||||
// Check that the default value (English) is set.
|
||||
await expect(app.currentNoteSplit).toContainText("Theme");
|
||||
const languageCombobox = await app.currentNoteSplit.getByRole("combobox").first();
|
||||
await expect(languageCombobox).toHaveValue("en");
|
||||
|
||||
// Select Chinese and ensure the translation is set.
|
||||
await languageCombobox.selectOption("cn");
|
||||
await expect(app.currentNoteSplit).toContainText("主题");
|
||||
|
||||
// Select English again.
|
||||
await languageCombobox.selectOption("en");
|
||||
await expect(app.currentNoteSplit).toContainText("Language");
|
||||
});
|
||||
@ -0,0 +1,59 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import App from "../support/app";
|
||||
|
||||
const NOTE_TITLE = "Trilium Integration Test DB";
|
||||
|
||||
test("Can drag tabs around", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
|
||||
// [1]: Trilium Integration Test DB note
|
||||
await app.closeAllTabs();
|
||||
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
|
||||
await expect(app.getActiveTab()).toContainText(NOTE_TITLE);
|
||||
|
||||
// [1] [2] [3]
|
||||
await app.addNewTab();
|
||||
await app.addNewTab();
|
||||
|
||||
let tab = app.getTab(0);
|
||||
|
||||
// Drag the first tab at the end
|
||||
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 }});
|
||||
|
||||
tab = app.getTab(2);
|
||||
await expect(tab).toContainText(NOTE_TITLE);
|
||||
|
||||
// Drag the tab to the left
|
||||
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 }});
|
||||
await expect(app.getTab(0)).toContainText(NOTE_TITLE);
|
||||
});
|
||||
|
||||
test("Can drag tab to new window", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
|
||||
await app.closeAllTabs();
|
||||
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
|
||||
const tab = app.getTab(0);
|
||||
await expect(tab).toContainText(NOTE_TITLE);
|
||||
|
||||
const popupPromise = page.waitForEvent("popup");
|
||||
|
||||
const tabPos = await tab.boundingBox();
|
||||
if (tabPos) {
|
||||
const x = tabPos.x + tabPos.width / 2;
|
||||
const y = tabPos.y + tabPos.height / 2;
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(x, y + tabPos.height + 100, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
} else {
|
||||
test.fail(true, "Unable to determine tab position");
|
||||
}
|
||||
|
||||
// Wait for the popup to show
|
||||
const popup = await popupPromise;
|
||||
const popupApp = new App(popup, context);
|
||||
await expect(popupApp.getActiveTab()).toHaveText(NOTE_TITLE);
|
||||
});
|
||||
@ -0,0 +1,45 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import App from "../support/app";
|
||||
|
||||
test("Displays lint warnings for backend script", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Backend script with lint warnings");
|
||||
|
||||
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
|
||||
|
||||
// Expect two warning signs in the gutter.
|
||||
expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2);
|
||||
|
||||
// Hover over hello
|
||||
await codeEditor.getByText("hello").first().hover();
|
||||
await expectTooltip(page, "'hello' is defined but never used.");
|
||||
|
||||
// Hover over world
|
||||
await codeEditor.getByText("world").first().hover();
|
||||
await expectTooltip(page, "'world' is defined but never used.");
|
||||
});
|
||||
|
||||
test("Displays lint errors for backend script", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Backend script with lint errors");
|
||||
|
||||
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
|
||||
|
||||
// Expect two warning signs in the gutter.
|
||||
const errorMarker = codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-error");
|
||||
await expect(errorMarker).toHaveCount(1);
|
||||
|
||||
// Hover over hello
|
||||
await errorMarker.hover();
|
||||
await expectTooltip(page, "Parsing error: Unexpected token world");
|
||||
});
|
||||
|
||||
async function expectTooltip(page: Page, tooltip: string) {
|
||||
await expect(page.locator(".CodeMirror-lint-tooltip:visible", {
|
||||
"hasText": tooltip
|
||||
})).toBeVisible();
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import App from "../support/app";
|
||||
|
||||
test("displays simple map", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Sample mindmap");
|
||||
|
||||
expect(app.currentNoteSplit).toContainText("Hello world");
|
||||
expect(app.currentNoteSplit).toContainText("1");
|
||||
expect(app.currentNoteSplit).toContainText("1a");
|
||||
});
|
||||
|
||||
test("displays note settings", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Sample mindmap");
|
||||
|
||||
await app.currentNoteSplit.getByText("Hello world").click({ force: true });
|
||||
const nodeMenu = app.currentNoteSplit.locator(".node-menu");
|
||||
expect(nodeMenu).toBeVisible();
|
||||
});
|
||||
@ -0,0 +1,51 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import App from "../support/app";
|
||||
|
||||
test("Table of contents is displayed", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Table of contents");
|
||||
|
||||
await expect(app.sidebar).toContainText("Table of Contents");
|
||||
const rootList = app.sidebar.locator(".toc-widget > span > ol");
|
||||
|
||||
// Heading 1.1
|
||||
// Heading 1.1
|
||||
// Heading 1.2
|
||||
// Heading 2
|
||||
// Heading 2.1
|
||||
// Heading 2.2
|
||||
// Heading 2.2.1
|
||||
// Heading 2.2.1.1
|
||||
// Heading 2.2.11.1
|
||||
|
||||
await expect(rootList.locator("> li")).toHaveCount(2);
|
||||
await expect(rootList.locator("> li").first()).toHaveText("Heading 1");
|
||||
await expect(rootList.locator("> ol").first().locator("> li").first()).toHaveText("Heading 1.1");
|
||||
await expect(rootList.locator("> ol").first().locator("> li").nth(1)).toHaveText("Heading 1.2");
|
||||
|
||||
// Heading 2 has a Katex equation, check if it's rendered.
|
||||
await expect(rootList.locator("> li").nth(1)).toContainText("Heading 2");
|
||||
await expect(rootList.locator("> li").nth(1).locator(".katex")).toBeAttached();
|
||||
|
||||
await expect(rootList.locator("> ol")).toHaveCount(2);
|
||||
await expect(rootList.locator("> ol").nth(1).locator("> li")).toHaveCount(2);
|
||||
await expect(rootList.locator("> ol").nth(1).locator("> ol")).toHaveCount(1);
|
||||
await expect(rootList.locator("> ol").nth(1).locator("> ol > ol")).toHaveCount(1);
|
||||
await expect(rootList.locator("> ol").nth(1).locator("> ol > ol > ol")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Highlights list is displayed", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Highlights list");
|
||||
|
||||
await expect(app.sidebar).toContainText("Highlights List");
|
||||
const rootList = app.sidebar.locator(".highlights-list ol");
|
||||
let index=0;
|
||||
for (const highlightedEl of [ "Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2" ]) {
|
||||
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,78 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
import type { BrowserContext } from "@playwright/test";
|
||||
|
||||
interface GotoOpts {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export default class App {
|
||||
readonly page: Page;
|
||||
readonly context: BrowserContext;
|
||||
|
||||
readonly tabBar: Locator;
|
||||
readonly noteTree: Locator;
|
||||
readonly currentNoteSplit: Locator;
|
||||
readonly sidebar: Locator;
|
||||
|
||||
constructor(page: Page, context: BrowserContext) {
|
||||
this.page = page;
|
||||
this.context = context;
|
||||
|
||||
this.tabBar = page.locator(".tab-row-widget-container");
|
||||
this.noteTree = page.locator(".tree-wrapper");
|
||||
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
|
||||
this.sidebar = page.locator("#right-pane");
|
||||
}
|
||||
|
||||
async goto(opts: GotoOpts = {}) {
|
||||
await this.context.addCookies([
|
||||
{
|
||||
url: "http://127.0.0.1:8082",
|
||||
name: "trilium-device",
|
||||
value: opts.isMobile ? "mobile" : "desktop"
|
||||
}
|
||||
]);
|
||||
|
||||
await this.page.goto("/", { waitUntil: "networkidle" });
|
||||
|
||||
// Wait for the page to load.
|
||||
await expect(this.page.locator(".tree"))
|
||||
.toContainText("Trilium Integration Test");
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
|
||||
async goToNoteInNewTab(noteTitle: string) {
|
||||
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
|
||||
await autocomplete.fill(noteTitle);
|
||||
await autocomplete.press("ArrowDown");
|
||||
await autocomplete.press("Enter");
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
await this.page.locator(".launcher-button.bx-cog").click();
|
||||
}
|
||||
|
||||
getTab(tabIndex: number) {
|
||||
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
|
||||
}
|
||||
|
||||
getActiveTab() {
|
||||
return this.tabBar.locator(".note-tab[active]");
|
||||
}
|
||||
|
||||
async closeAllTabs() {
|
||||
await this.getTab(0).click({ button: "right" });
|
||||
await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise
|
||||
await this.page.getByText("Close all tabs").click({ force: true });
|
||||
await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise
|
||||
}
|
||||
|
||||
async addNewTab() {
|
||||
await this.page.locator('[data-trigger-command="openNewTab"]').click();
|
||||
}
|
||||
|
||||
async clickNoteOnNoteTreeByTitle(title: string) {
|
||||
this.noteTree.getByText(title).click();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { initializeTranslations } from "./src/services/i18n.js";
|
||||
|
||||
await initializeTranslations();
|
||||
await import("./electron.js")
|
||||
await import("./electron.js");
|
||||
|
||||
@ -1,117 +1,115 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
|
||||
const APP_NAME = "TriliumNext Notes";
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
executableName: "trilium",
|
||||
name: APP_NAME,
|
||||
overwrite: true,
|
||||
asar: true,
|
||||
icon: "./images/app-icons/icon",
|
||||
extraResource: [
|
||||
// Moved to root
|
||||
...getExtraResourcesForPlatform(),
|
||||
packagerConfig: {
|
||||
executableName: "trilium",
|
||||
name: APP_NAME,
|
||||
overwrite: true,
|
||||
asar: true,
|
||||
icon: "./images/app-icons/icon",
|
||||
extraResource: [
|
||||
// Moved to root
|
||||
...getExtraResourcesForPlatform(),
|
||||
|
||||
// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
|
||||
"translations/",
|
||||
"node_modules/@highlightjs/cdn-assets/styles"
|
||||
],
|
||||
afterComplete: [(buildPath, _electronVersion, platform, _arch, callback) => {
|
||||
const extraResources = getExtraResourcesForPlatform();
|
||||
for (const resource of extraResources) {
|
||||
const baseName = path.basename(resource);
|
||||
let sourcePath;
|
||||
if (platform === 'darwin') {
|
||||
sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', baseName);
|
||||
} else {
|
||||
sourcePath = path.join(buildPath, 'resources', baseName);
|
||||
}
|
||||
let destPath;
|
||||
|
||||
if (baseName !== "256x256.png") {
|
||||
destPath = path.join(buildPath, baseName);
|
||||
} else {
|
||||
destPath = path.join(buildPath, "icon.png");
|
||||
}
|
||||
// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
|
||||
"translations/",
|
||||
"node_modules/@highlightjs/cdn-assets/styles"
|
||||
],
|
||||
afterComplete: [
|
||||
(buildPath, _electronVersion, platform, _arch, callback) => {
|
||||
const extraResources = getExtraResourcesForPlatform();
|
||||
for (const resource of extraResources) {
|
||||
const baseName = path.basename(resource);
|
||||
let sourcePath;
|
||||
if (platform === "darwin") {
|
||||
sourcePath = path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName);
|
||||
} else {
|
||||
sourcePath = path.join(buildPath, "resources", baseName);
|
||||
}
|
||||
let destPath;
|
||||
|
||||
// Copy files from resources folder to root
|
||||
fs.move(sourcePath, destPath)
|
||||
.then(() => callback())
|
||||
.catch(err => callback(err));
|
||||
}
|
||||
}]
|
||||
},
|
||||
rebuildConfig: {
|
||||
force: true
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
config: {
|
||||
options: {
|
||||
icon: "./images/app-icons/png/128x128.png",
|
||||
desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {
|
||||
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
|
||||
setupIcon: "./images/app-icons/icon.ico",
|
||||
loadingGif: "./images/app-icons/win/setup-banner.gif"
|
||||
}
|
||||
if (baseName !== "256x256.png") {
|
||||
destPath = path.join(buildPath, baseName);
|
||||
} else {
|
||||
destPath = path.join(buildPath, "icon.png");
|
||||
}
|
||||
|
||||
// Copy files from resources folder to root
|
||||
fs.move(sourcePath, destPath)
|
||||
.then(() => callback())
|
||||
.catch((err) => callback(err));
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
icon: "./images/app-icons/icon.icns",
|
||||
}
|
||||
rebuildConfig: {
|
||||
force: true
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
config: {
|
||||
options: {
|
||||
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
|
||||
icon: "./images/app-icons/icon.ico",
|
||||
makers: [
|
||||
{
|
||||
name: "@electron-forge/maker-deb",
|
||||
config: {
|
||||
options: {
|
||||
icon: "./images/app-icons/png/128x128.png",
|
||||
desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-squirrel",
|
||||
config: {
|
||||
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
|
||||
setupIcon: "./images/app-icons/icon.ico",
|
||||
loadingGif: "./images/app-icons/win/setup-banner.gif"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-dmg",
|
||||
config: {
|
||||
icon: "./images/app-icons/icon.icns"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "@electron-forge/maker-zip",
|
||||
config: {
|
||||
options: {
|
||||
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
|
||||
icon: "./images/app-icons/icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
name: "@electron-forge/plugin-auto-unpack-natives",
|
||||
config: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
function getExtraResourcesForPlatform() {
|
||||
let resources = [
|
||||
'dump-db/',
|
||||
'./bin/tpl/anonymize-database.sql'
|
||||
];
|
||||
const scripts = ['trilium-portable', 'trilium-safe-mode', 'trilium-no-cert-check']
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
for (const script of scripts) {
|
||||
resources.push(`./bin/tpl/${script}.bat`)
|
||||
}
|
||||
break;
|
||||
case 'darwin':
|
||||
break;
|
||||
case 'linux':
|
||||
resources.push("images/app-icons/png/256x256.png")
|
||||
for (const script of scripts) {
|
||||
resources.push(`./bin/tpl/${script}.sh`)
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
let resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"];
|
||||
const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
for (const script of scripts) {
|
||||
resources.push(`./bin/tpl/${script}.bat`);
|
||||
}
|
||||
break;
|
||||
case "darwin":
|
||||
break;
|
||||
case "linux":
|
||||
resources.push("images/app-icons/png/256x256.png");
|
||||
for (const script of scripts) {
|
||||
resources.push(`./bin/tpl/${script}.sh`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
[General]
|
||||
# Instance name can be used to distinguish between different instances using backend api.getInstanceName()
|
||||
instanceName=
|
||||
|
||||
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)
|
||||
noAuthentication=true
|
||||
|
||||
# set to true to disable backups (e.g. because of limited space on server)
|
||||
noBackup=false
|
||||
|
||||
# Disable automatically generating desktop icon
|
||||
# noDesktopIcon=true
|
||||
|
||||
[Network]
|
||||
# host setting is relevant only for web deployments - set the host on which the server will listen
|
||||
# host=0.0.0.0
|
||||
# port setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable)
|
||||
port=8080
|
||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
|
||||
https=false
|
||||
# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
|
||||
certPath=
|
||||
keyPath=
|
||||
# setting to give trust to reverse proxies, a comma-separated list of trusted rev. proxy IPs can be specified (CIDR notation is permitted),
|
||||
# alternatively 'true' will make use of the leftmost IP in X-Forwarded-For, ultimately an integer can be used to tell about the number of hops between
|
||||
# Trilium (which is hop 0) and the first trusted rev. proxy.
|
||||
# once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients.
|
||||
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
|
||||
trustedReverseProxy=false
|
||||
Binary file not shown.
@ -1,9 +1,9 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Can duplicate note with broken links", async ({ page }) => {
|
||||
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
|
||||
await page.locator('.tree-wrapper .fancytree-active').getByText('Note map').click({ button: 'right' });
|
||||
await page.getByText('Duplicate subtree').click();
|
||||
await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" });
|
||||
await page.getByText("Duplicate subtree").click();
|
||||
await expect(page.locator(".toast-body")).toBeHidden();
|
||||
await expect(page.locator('.tree-wrapper').getByText('Note map (dup)')).toBeVisible();
|
||||
await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible();
|
||||
});
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
test("has title", async ({ page }) => {
|
||||
await page.goto("https://playwright.dev/");
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test('get started link', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
test("get started link", async ({ page }) => {
|
||||
await page.goto("https://playwright.dev/");
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
// Click the get started link.
|
||||
await page.getByRole("link", { name: "Get started" }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
|
||||
});
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
test('Help popup', async ({ page }) => {
|
||||
await page.goto('http://localhost:8082');
|
||||
await page.getByText('Trilium Integration Test DB').click();
|
||||
test("Help popup", async ({ page }) => {
|
||||
await page.goto("http://localhost:8082");
|
||||
await page.getByText("Trilium Integration Test DB").click();
|
||||
|
||||
await page.locator('body').press('F1');
|
||||
await page.getByRole('link', { name: 'online↗' }).click();
|
||||
expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/")
|
||||
await page.locator("body").press("F1");
|
||||
await page.getByRole("link", { name: "online↗" }).click();
|
||||
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/");
|
||||
});
|
||||
|
||||
test('Complete help in search', async ({ page }) => {
|
||||
await page.goto('http://localhost:8082');
|
||||
test("Complete help in search", async ({ page }) => {
|
||||
await page.goto("http://localhost:8082");
|
||||
|
||||
// Clear all tabs
|
||||
await page.locator('.note-tab:first-of-type').locator("div").nth(1).click({ button: 'right' });
|
||||
await page.getByText('Close all tabs').click();
|
||||
await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" });
|
||||
await page.getByText("Close all tabs").click();
|
||||
|
||||
await page.locator('#launcher-container').getByRole('button', { name: '' }).first().click();
|
||||
await page.getByRole('cell', { name: ' ' }).locator('span').first().click();
|
||||
await page.getByRole('button', { name: 'complete help on search syntax' }).click();
|
||||
expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
||||
await page.locator("#launcher-container").getByRole("button", { name: "" }).first().click();
|
||||
await page.getByRole("cell", { name: " " }).locator("span").first().click();
|
||||
await page.getByRole("button", { name: "complete help on search syntax" }).click();
|
||||
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
||||
});
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
test("User can change language from settings", async ({ page }) => {
|
||||
await page.goto('http://localhost:8082');
|
||||
|
||||
// Clear all tabs
|
||||
await page.locator('.note-tab:first-of-type').locator("div").nth(1).click({ button: 'right' });
|
||||
await page.getByText('Close all tabs').click();
|
||||
|
||||
// Go to options -> Appearance
|
||||
await page.locator('#launcher-pane div').filter({ hasText: 'Options Open New Window' }).getByRole('button').click();
|
||||
await page.locator('#launcher-pane').getByText('Options').click();
|
||||
await page.locator('#center-pane').getByText('Appearance').click();
|
||||
|
||||
// Check that the default value (English) is set.
|
||||
await expect(page.locator('#center-pane')).toContainText('Theme');
|
||||
const languageCombobox = await page.getByRole('combobox').first();
|
||||
await expect(languageCombobox).toHaveValue("en");
|
||||
|
||||
// Select Chinese and ensure the translation is set.
|
||||
languageCombobox.selectOption("cn");
|
||||
await expect(page.locator('#center-pane')).toContainText('主题');
|
||||
|
||||
// Select English again.
|
||||
languageCombobox.selectOption("en");
|
||||
});
|
||||
|
||||
test("Restores language on start-up on desktop", async ({ page, context }) => {
|
||||
await page.goto('http://localhost:8082');
|
||||
await expect(page.locator('#launcher-pane').first()).toContainText("Open New Window");
|
||||
});
|
||||
|
||||
test("Restores language on start-up on mobile", async ({ page, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
url: "http://localhost:8082",
|
||||
name: "trilium-device",
|
||||
value: "mobile"
|
||||
}
|
||||
]);
|
||||
await page.goto('http://localhost:8082');
|
||||
await expect(page.locator('#launcher-pane div').first()).toContainText("Open New Window");
|
||||
});
|
||||
@ -1,21 +1,21 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
test("Native Title Bar not displayed on web", async ({ page }) => {
|
||||
await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsAppearance');
|
||||
await expect(page.getByRole('heading', { name: 'Theme' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Native Title Bar (requires' })).toBeHidden();
|
||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance");
|
||||
await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
|
||||
});
|
||||
|
||||
test("Tray settings not displayed on web", async ({ page }) => {
|
||||
await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsOther');
|
||||
await expect(page.getByRole('heading', { name: 'Note Erasure Timeout' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden();
|
||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther");
|
||||
await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
});
|
||||
|
||||
test("Spellcheck settings not displayed on web", async ({ page }) => {
|
||||
await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck');
|
||||
await expect(page.getByRole('heading', { name: 'Spell Check' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden();
|
||||
await expect(page.getByText('These options apply only for desktop builds')).toBeVisible();
|
||||
await expect(page.getByText('Enable spellcheck')).toBeHidden();
|
||||
await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck");
|
||||
await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
await expect(page.getByText("These options apply only for desktop builds")).toBeVisible();
|
||||
await expect(page.getByText("Enable spellcheck")).toBeHidden();
|
||||
});
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const expectedVersion = "0.90.3";
|
||||
|
||||
test("Displays update badge when there is a version available", async ({ page }) => {
|
||||
await page.goto("http://localhost:8080");
|
||||
await page.getByRole('button', { name: '' }).click();
|
||||
await page.getByRole("button", { name: "" }).click();
|
||||
await page.getByText(`Version ${expectedVersion} is available,`).click();
|
||||
|
||||
const page1 = await page.waitForEvent('popup');
|
||||
const page1 = await page.waitForEvent("popup");
|
||||
expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`);
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,100 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
async function validatorHtml(text, options) {
|
||||
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
|
||||
|
||||
if (result !== null) {
|
||||
// preceding code is copied over but any (non-newline) character is replaced with space
|
||||
// this will preserve line numbers etc.
|
||||
const prefix = text.substr(0, result.index).replace(/./g, " ");
|
||||
|
||||
const js = prefix + result[1];
|
||||
|
||||
return await validatorJavaScript(js, options);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function validatorJavaScript(text, options) {
|
||||
if (glob.isMobile()
|
||||
|| glob.getActiveContextNote() == null
|
||||
|| glob.getActiveContextNote().mime === 'application/json') {
|
||||
// eslint doesn't seem to validate pure JSON well
|
||||
return [];
|
||||
}
|
||||
|
||||
await glob.requireLibrary(glob.ESLINT);
|
||||
|
||||
if (text.length > 20000) {
|
||||
console.log("Skipping linting because of large size: ", text.length);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors = new eslint().verify(text, {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest"
|
||||
},
|
||||
extends: ['eslint:recommended', 'airbnb-base'],
|
||||
env: {
|
||||
'browser': true,
|
||||
'node': true
|
||||
},
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'func-names': 'off',
|
||||
'comma-dangle': ['warn'],
|
||||
'padded-blocks': 'off',
|
||||
'linebreak-style': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }],
|
||||
'no-nested-ternary': 'off',
|
||||
'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}]
|
||||
},
|
||||
globals: {
|
||||
"api": "readonly"
|
||||
}
|
||||
});
|
||||
|
||||
console.log(errors);
|
||||
|
||||
const result = [];
|
||||
if (errors) {
|
||||
parseErrors(errors, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
|
||||
CodeMirror.registerHelper("lint", "html", validatorHtml);
|
||||
|
||||
function parseErrors(errors, output) {
|
||||
for (const error of errors) {
|
||||
const startLine = error.line - 1;
|
||||
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
|
||||
const startCol = error.column - 1;
|
||||
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
|
||||
|
||||
output.push({
|
||||
message: error.message,
|
||||
severity: error.severity === 1 ? "warning" : "error",
|
||||
from: CodeMirror.Pos(startLine, startCol),
|
||||
to: CodeMirror.Pos(endLine, endCol)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@ -1,15 +1,12 @@
|
||||
{
|
||||
"restartable": "rs",
|
||||
"ignore": [".git", "node_modules/**/node_modules", "src/public/"],
|
||||
"verbose": false,
|
||||
"exec": "tsx",
|
||||
"watch": [
|
||||
"src/",
|
||||
"translations/"
|
||||
],
|
||||
"signal": "SIGTERM",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"ext": "ts,js,json"
|
||||
"restartable": "rs",
|
||||
"ignore": [".git", "node_modules/**/node_modules", "src/public/"],
|
||||
"verbose": false,
|
||||
"exec": "tsx",
|
||||
"watch": ["src/", "translations/"],
|
||||
"signal": "SIGTERM",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"ext": "ts,js,json"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,43 +1,39 @@
|
||||
import sanitizeAttributeName from "../src/services/sanitize_attribute_name"
|
||||
import sanitizeAttributeName from "../src/services/sanitize_attribute_name";
|
||||
import { describe, it, execute, expect } from "./mini_test";
|
||||
|
||||
// fn value, expected value
|
||||
const testCases: [fnValue: string, expectedValue: string][] = [
|
||||
["testName", "testName"],
|
||||
["test_name", "test_name"],
|
||||
["test with space", "test_with_space"],
|
||||
["test:with:colon", "test:with:colon"],
|
||||
["testName", "testName"],
|
||||
["test_name", "test_name"],
|
||||
["test with space", "test_with_space"],
|
||||
["test:with:colon", "test:with:colon"],
|
||||
|
||||
// numbers
|
||||
["123456", "123456"],
|
||||
["123:456", "123:456"],
|
||||
["123456 abc", "123456_abc"],
|
||||
// numbers
|
||||
["123456", "123456"],
|
||||
["123:456", "123:456"],
|
||||
["123456 abc", "123456_abc"],
|
||||
|
||||
// non-latin characters
|
||||
["ε", "ε"],
|
||||
["attribute ε", "attribute_ε"],
|
||||
|
||||
|
||||
// special characters
|
||||
["test/name", "test_name"],
|
||||
["test%name", "test_name"],
|
||||
["\/", "_"],
|
||||
|
||||
// empty string
|
||||
["", "unnamed"],
|
||||
]
|
||||
// non-latin characters
|
||||
["ε", "ε"],
|
||||
["attribute ε", "attribute_ε"],
|
||||
|
||||
// special characters
|
||||
["test/name", "test_name"],
|
||||
["test%name", "test_name"],
|
||||
["\/", "_"],
|
||||
|
||||
// empty string
|
||||
["", "unnamed"]
|
||||
];
|
||||
|
||||
describe("sanitizeAttributeName unit tests", () => {
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
|
||||
const [value, expected] = testCase;
|
||||
const actual = sanitizeAttributeName(value);
|
||||
expect(actual).toEqual(expected);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
execute()
|
||||
testCases.forEach((testCase) => {
|
||||
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
|
||||
const [value, expected] = testCase;
|
||||
const actual = sanitizeAttributeName(value);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
execute();
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import etapi from "../support/etapi.js";
|
||||
|
||||
etapi.describeEtapi("app_info", () => {
|
||||
it("get", async () => {
|
||||
const appInfo = await etapi.getEtapi("app-info");
|
||||
expect(appInfo.clipperProtocolVersion).toEqual("1.0");
|
||||
});
|
||||
it("get", async () => {
|
||||
const appInfo = await etapi.getEtapi("app-info");
|
||||
expect(appInfo.clipperProtocolVersion).toEqual("1.0");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import etapi from "../support/etapi.js";
|
||||
|
||||
etapi.describeEtapi("backup", () => {
|
||||
it("create", async () => {
|
||||
const response = await etapi.putEtapiContent("backup/etapi_test");
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
it("create", async () => {
|
||||
const response = await etapi.putEtapiContent("backup/etapi_test");
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
describe("Notes", () => {
|
||||
it("zzz", () => {
|
||||
|
||||
});
|
||||
it("zzz", () => {});
|
||||
});
|
||||
|
||||
@ -1,256 +1,162 @@
|
||||
import lex from "../../src/services/search/services/lex.js";
|
||||
|
||||
describe("Lexer fulltext", () => {
|
||||
it("simple lexing", () => {
|
||||
expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual([
|
||||
"hello",
|
||||
"world",
|
||||
]);
|
||||
|
||||
expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual([
|
||||
"hello",
|
||||
"world",
|
||||
]);
|
||||
});
|
||||
|
||||
it("use quotes to keep words together", () => {
|
||||
expect(
|
||||
lex("'hello world' my friend").fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["hello world", "my", "friend"]);
|
||||
|
||||
expect(
|
||||
lex('"hello world" my friend').fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["hello world", "my", "friend"]);
|
||||
|
||||
expect(
|
||||
lex("`hello world` my friend").fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["hello world", "my", "friend"]);
|
||||
});
|
||||
|
||||
it("you can use different quotes and other special characters inside quotes", () => {
|
||||
expect(
|
||||
lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map(
|
||||
(t) => t.token
|
||||
)
|
||||
).toEqual(['i can use " or ` or #~=*', "without", "problem"]);
|
||||
});
|
||||
|
||||
it("I can use backslash to escape quotes", () => {
|
||||
expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(
|
||||
["hello", '"world"']
|
||||
);
|
||||
|
||||
expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual(
|
||||
["hello", "'world'"]
|
||||
);
|
||||
|
||||
expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual(
|
||||
["hello", "`world`"]
|
||||
);
|
||||
|
||||
expect(
|
||||
lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)
|
||||
).toEqual(['hello "world"']);
|
||||
|
||||
expect(
|
||||
lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["hello 'world'"]);
|
||||
|
||||
expect(
|
||||
lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["hello `world`"]);
|
||||
|
||||
expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual([
|
||||
"#token",
|
||||
]);
|
||||
});
|
||||
|
||||
it("quote inside a word does not have a special meaning", () => {
|
||||
const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan");
|
||||
|
||||
expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual([
|
||||
"d'artagnan",
|
||||
"is",
|
||||
"dead",
|
||||
]);
|
||||
|
||||
expect(lexResult.expressionTokens.map((t) => t.token)).toEqual([
|
||||
"#hero",
|
||||
"=",
|
||||
"d'artagnan",
|
||||
]);
|
||||
});
|
||||
|
||||
it("if quote is not ended then it's just one long token", () => {
|
||||
expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual(
|
||||
["unfinished quote"]
|
||||
);
|
||||
});
|
||||
|
||||
it("parenthesis and symbols in fulltext section are just normal characters", () => {
|
||||
expect(
|
||||
lex("what's u=p <b(r*t)h>").fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["what's", "u=p", "<b(r*t)h>"]);
|
||||
});
|
||||
|
||||
it("operator characters in expressions are separate tokens", () => {
|
||||
expect(
|
||||
lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token)
|
||||
).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
|
||||
});
|
||||
|
||||
it("escaping special characters", () => {
|
||||
expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual([
|
||||
"hello",
|
||||
"#~'",
|
||||
]);
|
||||
});
|
||||
it("simple lexing", () => {
|
||||
expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]);
|
||||
|
||||
expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]);
|
||||
});
|
||||
|
||||
it("use quotes to keep words together", () => {
|
||||
expect(lex("'hello world' my friend").fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]);
|
||||
|
||||
expect(lex('"hello world" my friend').fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]);
|
||||
|
||||
expect(lex("`hello world` my friend").fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]);
|
||||
});
|
||||
|
||||
it("you can use different quotes and other special characters inside quotes", () => {
|
||||
expect(lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map((t) => t.token)).toEqual(['i can use " or ` or #~=*', "without", "problem"]);
|
||||
});
|
||||
|
||||
it("I can use backslash to escape quotes", () => {
|
||||
expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(["hello", '"world"']);
|
||||
|
||||
expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "'world'"]);
|
||||
|
||||
expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual(["hello", "`world`"]);
|
||||
|
||||
expect(lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(['hello "world"']);
|
||||
|
||||
expect(lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)).toEqual(["hello 'world'"]);
|
||||
|
||||
expect(lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)).toEqual(["hello `world`"]);
|
||||
|
||||
expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual(["#token"]);
|
||||
});
|
||||
|
||||
it("quote inside a word does not have a special meaning", () => {
|
||||
const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan");
|
||||
|
||||
expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual(["d'artagnan", "is", "dead"]);
|
||||
|
||||
expect(lexResult.expressionTokens.map((t) => t.token)).toEqual(["#hero", "=", "d'artagnan"]);
|
||||
});
|
||||
|
||||
it("if quote is not ended then it's just one long token", () => {
|
||||
expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual(["unfinished quote"]);
|
||||
});
|
||||
|
||||
it("parenthesis and symbols in fulltext section are just normal characters", () => {
|
||||
expect(lex("what's u=p <b(r*t)h>").fulltextTokens.map((t) => t.token)).toEqual(["what's", "u=p", "<b(r*t)h>"]);
|
||||
});
|
||||
|
||||
it("operator characters in expressions are separate tokens", () => {
|
||||
expect(lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token)).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
|
||||
});
|
||||
|
||||
it("escaping special characters", () => {
|
||||
expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "#~'"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Lexer expression", () => {
|
||||
it("simple attribute existence", () => {
|
||||
expect(
|
||||
lex("#label ~relation").expressionTokens.map((t) => t.token)
|
||||
).toEqual(["#label", "~relation"]);
|
||||
});
|
||||
|
||||
it("simple label operators", () => {
|
||||
expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual([
|
||||
"#label",
|
||||
"*=*",
|
||||
"text",
|
||||
]);
|
||||
});
|
||||
|
||||
it("simple label operator with in quotes", () => {
|
||||
expect(lex("#label*=*'text'").expressionTokens).toEqual([
|
||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
||||
{ token: "text", inQuotes: true, startIndex: 10, endIndex: 13 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("simple label operator with param without quotes", () => {
|
||||
expect(lex("#label*=*text").expressionTokens).toEqual([
|
||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
||||
{ token: "text", inQuotes: false, startIndex: 9, endIndex: 12 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("simple label operator with empty string param", () => {
|
||||
expect(lex("#label = ''").expressionTokens).toEqual([
|
||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||
{ token: "=", inQuotes: false, startIndex: 7, endIndex: 7 },
|
||||
// weird case for empty strings which ends up with endIndex < startIndex :-(
|
||||
{ token: "", inQuotes: true, startIndex: 10, endIndex: 9 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("note. prefix also separates fulltext from expression", () => {
|
||||
expect(
|
||||
lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(
|
||||
(t) => t.token
|
||||
)
|
||||
).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);
|
||||
});
|
||||
|
||||
it("note. prefix in quotes will note start expression", () => {
|
||||
expect(
|
||||
lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token)
|
||||
).toEqual(["hello", "fulltext", "note.txt"]);
|
||||
});
|
||||
|
||||
it("complex expressions with and, or and parenthesis", () => {
|
||||
expect(
|
||||
lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map(
|
||||
(t) => t.token
|
||||
)
|
||||
).toEqual([
|
||||
"#",
|
||||
"(",
|
||||
"#label",
|
||||
"=",
|
||||
"text",
|
||||
"or",
|
||||
"#second",
|
||||
"=",
|
||||
"text",
|
||||
")",
|
||||
"and",
|
||||
"~relation",
|
||||
]);
|
||||
});
|
||||
|
||||
it("dot separated properties", () => {
|
||||
expect(
|
||||
lex(
|
||||
`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`
|
||||
).expressionTokens.map((t) => t.token)
|
||||
).toEqual([
|
||||
"#",
|
||||
"~author",
|
||||
".",
|
||||
"title",
|
||||
"=",
|
||||
"hugh howey",
|
||||
"and",
|
||||
"note",
|
||||
".",
|
||||
"book title",
|
||||
"=",
|
||||
"silo",
|
||||
]);
|
||||
});
|
||||
|
||||
it("negation of label and relation", () => {
|
||||
expect(
|
||||
lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token)
|
||||
).toEqual(["#!capital", "~!neighbor"]);
|
||||
});
|
||||
|
||||
it("negation of sub-expression", () => {
|
||||
expect(
|
||||
lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map(
|
||||
(t) => t.token
|
||||
)
|
||||
).toEqual([
|
||||
"#",
|
||||
"not",
|
||||
"(",
|
||||
"#capital",
|
||||
")",
|
||||
"and",
|
||||
"note",
|
||||
".",
|
||||
"noteid",
|
||||
"!=",
|
||||
"root",
|
||||
]);
|
||||
});
|
||||
|
||||
it("order by multiple labels", () => {
|
||||
expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual(
|
||||
["#", "orderby", "#a", ",", "#b"]
|
||||
);
|
||||
});
|
||||
it("simple attribute existence", () => {
|
||||
expect(lex("#label ~relation").expressionTokens.map((t) => t.token)).toEqual(["#label", "~relation"]);
|
||||
});
|
||||
|
||||
it("simple label operators", () => {
|
||||
expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual(["#label", "*=*", "text"]);
|
||||
});
|
||||
|
||||
it("simple label operator with in quotes", () => {
|
||||
expect(lex("#label*=*'text'").expressionTokens).toEqual([
|
||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
||||
{ token: "text", inQuotes: true, startIndex: 10, endIndex: 13 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("simple label operator with param without quotes", () => {
|
||||
expect(lex("#label*=*text").expressionTokens).toEqual([
|
||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
||||
{ token: "text", inQuotes: false, startIndex: 9, endIndex: 12 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("simple label operator with empty string param", () => {
|
||||
expect(lex("#label = ''").expressionTokens).toEqual([
|
||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||
{ token: "=", inQuotes: false, startIndex: 7, endIndex: 7 },
|
||||
// weird case for empty strings which ends up with endIndex < startIndex :-(
|
||||
{ token: "", inQuotes: true, startIndex: 10, endIndex: 9 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("note. prefix also separates fulltext from expression", () => {
|
||||
expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map((t) => t.token)).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);
|
||||
});
|
||||
|
||||
it("note. prefix in quotes will note start expression", () => {
|
||||
expect(lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)).toEqual([]);
|
||||
|
||||
expect(lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token)).toEqual(["hello", "fulltext", "note.txt"]);
|
||||
});
|
||||
|
||||
it("complex expressions with and, or and parenthesis", () => {
|
||||
expect(lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map((t) => t.token)).toEqual([
|
||||
"#",
|
||||
"(",
|
||||
"#label",
|
||||
"=",
|
||||
"text",
|
||||
"or",
|
||||
"#second",
|
||||
"=",
|
||||
"text",
|
||||
")",
|
||||
"and",
|
||||
"~relation"
|
||||
]);
|
||||
});
|
||||
|
||||
it("dot separated properties", () => {
|
||||
expect(lex(`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`).expressionTokens.map((t) => t.token)).toEqual([
|
||||
"#",
|
||||
"~author",
|
||||
".",
|
||||
"title",
|
||||
"=",
|
||||
"hugh howey",
|
||||
"and",
|
||||
"note",
|
||||
".",
|
||||
"book title",
|
||||
"=",
|
||||
"silo"
|
||||
]);
|
||||
});
|
||||
|
||||
it("negation of label and relation", () => {
|
||||
expect(lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token)).toEqual(["#!capital", "~!neighbor"]);
|
||||
});
|
||||
|
||||
it("negation of sub-expression", () => {
|
||||
expect(lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map((t) => t.token)).toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]);
|
||||
});
|
||||
|
||||
it("order by multiple labels", () => {
|
||||
expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual(["#", "orderby", "#a", ",", "#b"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Lexer invalid queries and edge cases", () => {
|
||||
it("concatenated attributes", () => {
|
||||
expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual(
|
||||
["#label", "~relation"]
|
||||
);
|
||||
});
|
||||
|
||||
it("trailing escape \\", () => {
|
||||
expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual([
|
||||
"abc",
|
||||
"\\",
|
||||
]);
|
||||
});
|
||||
it("concatenated attributes", () => {
|
||||
expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual(["#label", "~relation"]);
|
||||
});
|
||||
|
||||
it("trailing escape \\", () => {
|
||||
expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual(["abc", "\\"]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,319 +1,355 @@
|
||||
// @ts-nocheck
|
||||
// There are many issues with the types of the parser e.g. "parse" function returns "Expression"
|
||||
// but we access properties like "subExpressions" which is not defined in the "Expression" class.
|
||||
|
||||
import AndExp from "../../src/services/search/expressions/and.js";
|
||||
import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js";
|
||||
import Expression from "../../src/services/search/expressions/expression.js";
|
||||
import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js";
|
||||
import NotExp from "../../src/services/search/expressions/not.js";
|
||||
import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js";
|
||||
import NoteFlatTextExp from "../../src/services/search/expressions/note_flat_text.js";
|
||||
import OrExp from "../../src/services/search/expressions/or.js";
|
||||
import OrderByAndLimitExp from "../../src/services/search/expressions/order_by_and_limit.js";
|
||||
import PropertyComparisonExp from "../../src/services/search/expressions/property_comparison.js";
|
||||
import SearchContext from "../../src/services/search/search_context.js";
|
||||
import parse from "../../src/services/search/services/parse.js";
|
||||
|
||||
function tokens(toks: Array<string>, cur = 0): Array<any> {
|
||||
return toks.map((arg) => {
|
||||
if (Array.isArray(arg)) {
|
||||
return tokens(arg, cur);
|
||||
} else {
|
||||
cur += arg.length;
|
||||
|
||||
return {
|
||||
token: arg,
|
||||
inQuotes: false,
|
||||
startIndex: cur - arg.length,
|
||||
endIndex: cur - 1,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js";
|
||||
|
||||
function assertIsArchived(exp: Expression) {
|
||||
expect(exp.constructor.name).toEqual('PropertyComparisonExp');
|
||||
expect(exp.propertyName).toEqual('isArchived');
|
||||
expect(exp.operator).toEqual('=');
|
||||
expect(exp.comparedValue).toEqual('false');
|
||||
}
|
||||
|
||||
describe('Parser', () => {
|
||||
it('fulltext parser without content', () => {
|
||||
describe("Parser", () => {
|
||||
it("fulltext parser without content", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: tokens(['hello', 'hi']),
|
||||
fulltextTokens: tokens(["hello", "hi"]),
|
||||
expressionTokens: [],
|
||||
searchContext: new SearchContext({ excludeArchived: true }),
|
||||
});
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
expect(rootExp.subExpressions[0].constructor.name).toEqual('PropertyComparisonExp');
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
||||
expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual('NoteFlatTextExp');
|
||||
expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(['hello', 'hi']);
|
||||
expectExpression(rootExp.subExpressions[0], PropertyComparisonExp);
|
||||
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
|
||||
const flatTextExp = expectExpression(orExp.subExpressions[0], NoteFlatTextExp);
|
||||
expect(flatTextExp.tokens).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
it('fulltext parser with content', () => {
|
||||
it("fulltext parser with content", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: tokens(['hello', 'hi']),
|
||||
fulltextTokens: tokens(["hello", "hi"]),
|
||||
expressionTokens: [],
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
||||
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
|
||||
|
||||
const subs = rootExp.subExpressions[2].subExpressions;
|
||||
const firstSub = expectExpression(orExp.subExpressions[0], NoteFlatTextExp);
|
||||
expect(firstSub.tokens).toEqual(["hello", "hi"]);
|
||||
|
||||
expect(subs[0].constructor.name).toEqual('NoteFlatTextExp');
|
||||
expect(subs[0].tokens).toEqual(['hello', 'hi']);
|
||||
|
||||
expect(subs[1].constructor.name).toEqual('NoteContentFulltextExp');
|
||||
expect(subs[1].tokens).toEqual(['hello', 'hi']);
|
||||
const secondSub = expectExpression(orExp.subExpressions[1], NoteContentFulltextExp);
|
||||
expect(secondSub.tokens).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
it('simple label comparison', () => {
|
||||
it("simple label comparison", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#mylabel', '=', 'text']),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["#mylabel", "=", "text"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(rootExp.subExpressions[2].attributeType).toEqual('label');
|
||||
expect(rootExp.subExpressions[2].attributeName).toEqual('mylabel');
|
||||
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
||||
const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp);
|
||||
expect(labelComparisonExp.attributeType).toEqual("label");
|
||||
expect(labelComparisonExp.attributeName).toEqual("mylabel");
|
||||
expect(labelComparisonExp.comparator).toBeTruthy();
|
||||
});
|
||||
|
||||
it('simple attribute negation', () => {
|
||||
it("simple attribute negation", () => {
|
||||
let rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#!mylabel']),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["#!mylabel"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp');
|
||||
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp');
|
||||
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('label');
|
||||
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('mylabel');
|
||||
let notExp = expectExpression(rootExp.subExpressions[2], NotExp);
|
||||
let attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp);
|
||||
expect(attributeExistsExp.attributeType).toEqual("label");
|
||||
expect(attributeExistsExp.attributeName).toEqual("mylabel");
|
||||
|
||||
rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['~!myrelation']),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["~!myrelation"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp');
|
||||
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp');
|
||||
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('relation');
|
||||
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('myrelation');
|
||||
notExp = expectExpression(rootExp.subExpressions[2], NotExp);
|
||||
attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp);
|
||||
expect(attributeExistsExp.attributeType).toEqual("relation");
|
||||
expect(attributeExistsExp.attributeName).toEqual("myrelation");
|
||||
});
|
||||
|
||||
it('simple label AND', () => {
|
||||
it("simple label AND", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=', 'text', 'and', '#second', '=', 'text']),
|
||||
searchContext: new SearchContext(true),
|
||||
});
|
||||
expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
|
||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(firstSub.attributeName).toEqual('first');
|
||||
const andExp = expectExpression(rootExp.subExpressions[2], AndExp);
|
||||
const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp);
|
||||
|
||||
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(secondSub.attributeName).toEqual('second');
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it('simple label AND without explicit AND', () => {
|
||||
it("simple label AND without explicit AND", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=', 'text', '#second', '=', 'text']),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
|
||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(firstSub.attributeName).toEqual('first');
|
||||
const andExp = expectExpression(rootExp.subExpressions[2], AndExp);
|
||||
const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp);
|
||||
|
||||
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(secondSub.attributeName).toEqual('second');
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it('simple label OR', () => {
|
||||
it("simple label OR", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=', 'text', 'or', '#second', '=', 'text']),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(firstSub.attributeName).toEqual('first');
|
||||
|
||||
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(secondSub.attributeName).toEqual('second');
|
||||
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
|
||||
const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, LabelComparisonExp);
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it('fulltext and simple label', () => {
|
||||
it("fulltext and simple label", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: tokens(['hello']),
|
||||
expressionTokens: tokens(['#mylabel', '=', 'text']),
|
||||
searchContext: new SearchContext({ excludeArchived: true }),
|
||||
});
|
||||
fulltextTokens: tokens(["hello"]),
|
||||
expressionTokens: tokens(["#mylabel", "=", "text"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions;
|
||||
const [firstSub, _, thirdSub, fourth] = expectSubexpressions(rootExp, PropertyComparisonExp, undefined, OrExp, LabelComparisonExp);
|
||||
|
||||
expect(firstSub.constructor.name).toEqual('PropertyComparisonExp');
|
||||
expect(firstSub.propertyName).toEqual('isArchived');
|
||||
expect(firstSub.propertyName).toEqual("isArchived");
|
||||
|
||||
expect(thirdSub.constructor.name).toEqual('OrExp');
|
||||
expect(thirdSub.subExpressions[0].constructor.name).toEqual('NoteFlatTextExp');
|
||||
expect(thirdSub.subExpressions[0].tokens).toEqual(['hello']);
|
||||
const noteFlatTextExp = expectExpression(thirdSub.subExpressions[0], NoteFlatTextExp);
|
||||
expect(noteFlatTextExp.tokens).toEqual(["hello"]);
|
||||
|
||||
expect(fourth.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(fourth.attributeName).toEqual('mylabel');
|
||||
expect(fourth.attributeName).toEqual("mylabel");
|
||||
});
|
||||
|
||||
it('label sub-expression', () => {
|
||||
it("label sub-expression", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=', 'text', 'or', ['#second', '=', 'text', 'and', '#third', '=', 'text']]),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(firstSub.attributeName).toEqual('first');
|
||||
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
|
||||
const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, AndExp);
|
||||
|
||||
expect(secondSub.constructor.name).toEqual('AndExp');
|
||||
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(firstSubSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(firstSubSub.attributeName).toEqual('second');
|
||||
|
||||
expect(secondSubSub.constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(secondSubSub.attributeName).toEqual('third');
|
||||
const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, LabelComparisonExp, LabelComparisonExp);
|
||||
expect(firstSubSub.attributeName).toEqual("second");
|
||||
expect(secondSubSub.attributeName).toEqual("third");
|
||||
});
|
||||
|
||||
it('label sub-expression without explicit operator', () => {
|
||||
it("label sub-expression without explicit operator", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', ['#second', 'or', '#third'], '#fourth']),
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]),
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
|
||||
const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions;
|
||||
const andExp = expectExpression(rootExp.subExpressions[2], AndExp);
|
||||
const [firstSub, secondSub, thirdSub] = expectSubexpressions(andExp, AttributeExistsExp, OrExp, AttributeExistsExp);
|
||||
|
||||
expect(firstSub.constructor.name).toEqual('AttributeExistsExp');
|
||||
expect(firstSub.attributeName).toEqual('first');
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual('OrExp');
|
||||
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
||||
const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, AttributeExistsExp, AttributeExistsExp);
|
||||
expect(firstSubSub.attributeName).toEqual("second");
|
||||
expect(secondSubSub.attributeName).toEqual("third");
|
||||
|
||||
expect(firstSubSub.constructor.name).toEqual('AttributeExistsExp');
|
||||
expect(firstSubSub.attributeName).toEqual('second');
|
||||
expect(thirdSub.attributeName).toEqual("fourth");
|
||||
});
|
||||
|
||||
expect(secondSubSub.constructor.name).toEqual('AttributeExistsExp');
|
||||
expect(secondSubSub.attributeName).toEqual('third');
|
||||
it("parses limit without order by", () => {
|
||||
const rootExp = parse({
|
||||
fulltextTokens: tokens(["hello", "hi"]),
|
||||
expressionTokens: [],
|
||||
searchContext: new SearchContext({ limit: 2 })
|
||||
}, OrderByAndLimitExp);
|
||||
|
||||
expect(thirdSub.constructor.name).toEqual('AttributeExistsExp');
|
||||
expect(thirdSub.attributeName).toEqual('fourth');
|
||||
expect(rootExp.limit).toBe(2);
|
||||
expect(rootExp.subExpression).toBeInstanceOf(AndExp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid expressions', () => {
|
||||
it('incomplete comparison', () => {
|
||||
describe("Invalid expressions", () => {
|
||||
it("incomplete comparison", () => {
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
parse({
|
||||
parseInternal({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=']),
|
||||
searchContext,
|
||||
expressionTokens: tokens(["#first", "="]),
|
||||
searchContext
|
||||
});
|
||||
|
||||
expect(searchContext.error).toEqual('Misplaced or incomplete expression "="');
|
||||
});
|
||||
|
||||
it('comparison between labels is impossible', () => {
|
||||
it("comparison between labels is impossible", () => {
|
||||
let searchContext = new SearchContext();
|
||||
searchContext.originalQuery = '#first = #second';
|
||||
searchContext.originalQuery = "#first = #second";
|
||||
|
||||
parse({
|
||||
parseInternal({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=', '#second']),
|
||||
searchContext,
|
||||
expressionTokens: tokens(["#first", "=", "#second"]),
|
||||
searchContext
|
||||
});
|
||||
|
||||
expect(searchContext.error).toEqual(
|
||||
`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`
|
||||
);
|
||||
expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`);
|
||||
|
||||
searchContext = new SearchContext();
|
||||
searchContext.originalQuery = '#first = note.relations.second';
|
||||
searchContext.originalQuery = "#first = note.relations.second";
|
||||
|
||||
parse({
|
||||
parseInternal({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['#first', '=', 'note', '.', 'relations', 'second']),
|
||||
searchContext,
|
||||
expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]),
|
||||
searchContext
|
||||
});
|
||||
|
||||
expect(searchContext.error).toEqual(
|
||||
`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`
|
||||
);
|
||||
expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`);
|
||||
|
||||
const rootExp = parse({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: [
|
||||
{ token: '#first', inQuotes: false },
|
||||
{ token: '=', inQuotes: false },
|
||||
{ token: '#second', inQuotes: true },
|
||||
{ token: "#first", inQuotes: false },
|
||||
{ token: "=", inQuotes: false },
|
||||
{ token: "#second", inQuotes: true }
|
||||
],
|
||||
searchContext: new SearchContext(),
|
||||
});
|
||||
searchContext: new SearchContext()
|
||||
}, AndExp);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
||||
assertIsArchived(rootExp.subExpressions[0]);
|
||||
|
||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp');
|
||||
expect(rootExp.subExpressions[2].attributeType).toEqual('label');
|
||||
expect(rootExp.subExpressions[2].attributeName).toEqual('first');
|
||||
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
||||
const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp);
|
||||
expect(labelComparisonExp.attributeType).toEqual("label");
|
||||
expect(labelComparisonExp.attributeName).toEqual("first");
|
||||
expect(labelComparisonExp.comparator).toBeTruthy();
|
||||
});
|
||||
|
||||
it('searching by relation without note property', () => {
|
||||
it("searching by relation without note property", () => {
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
parse({
|
||||
parseInternal({
|
||||
fulltextTokens: [],
|
||||
expressionTokens: tokens(['~first', '=', 'text', '-', 'abc']),
|
||||
searchContext,
|
||||
expressionTokens: tokens(["~first", "=", "text", "-", "abc"]),
|
||||
searchContext
|
||||
});
|
||||
|
||||
expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""');
|
||||
});
|
||||
});
|
||||
|
||||
type ClassType<T extends Expression> = new (...args: any[]) => T;
|
||||
|
||||
function tokens(toks: (string | string[])[], cur = 0): Array<any> {
|
||||
return toks.map((arg) => {
|
||||
if (Array.isArray(arg)) {
|
||||
return tokens(arg, cur);
|
||||
} else {
|
||||
cur += arg.length;
|
||||
|
||||
return {
|
||||
token: arg,
|
||||
inQuotes: false,
|
||||
startIndex: cur - arg.length,
|
||||
endIndex: cur - 1
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function assertIsArchived(_exp: Expression) {
|
||||
const exp = expectExpression(_exp, PropertyComparisonExp);
|
||||
expect(exp.propertyName).toEqual("isArchived");
|
||||
expect(exp.operator).toEqual("=");
|
||||
expect(exp.comparedValue).toEqual("false");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the corresponding {@link Expression} from plain text, while also expecting the resulting expression to be of the given type.
|
||||
*
|
||||
* @param opts the options for parsing.
|
||||
* @param type the expected type of the expression.
|
||||
* @returns the expression typecasted to the expected type.
|
||||
*/
|
||||
function parse<T extends Expression>(opts: ParseOpts, type: ClassType<T>) {
|
||||
return expectExpression(parseInternal(opts), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the given {@link Expression} to be of the given type.
|
||||
*
|
||||
* @param exp an instance of an {@link Expression}.
|
||||
* @param type a type class such as {@link AndExp}, {@link OrExp}, etc.
|
||||
* @returns the same expression typecasted to the expected type.
|
||||
*/
|
||||
function expectExpression<T extends Expression>(exp: Expression, type: ClassType<T>) {
|
||||
expect(exp).toBeInstanceOf(type);
|
||||
return exp as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* For an {@link AndExp}, it goes through all its subexpressions (up to fourth) and checks their type and returns them as a typecasted array.
|
||||
* Each subexpression can have their own type.
|
||||
*
|
||||
* @param exp the expression containing one or more subexpressions.
|
||||
* @param firstType the type of the first subexpression.
|
||||
* @param secondType the type of the second subexpression.
|
||||
* @param thirdType the type of the third subexpression.
|
||||
* @param fourthType the type of the fourth subexpression.
|
||||
* @returns an array of all the subexpressions (in order) typecasted to their expected type.
|
||||
*/
|
||||
function expectSubexpressions<FirstT extends Expression,
|
||||
SecondT extends Expression,
|
||||
ThirdT extends Expression,
|
||||
FourthT extends Expression>(
|
||||
exp: AndExp,
|
||||
firstType: ClassType<FirstT>,
|
||||
secondType?: ClassType<SecondT>,
|
||||
thirdType?: ClassType<ThirdT>,
|
||||
fourthType?: ClassType<FourthT>): [ FirstT, SecondT, ThirdT, FourthT ]
|
||||
{
|
||||
expectExpression(exp.subExpressions[0], firstType);
|
||||
if (secondType) {
|
||||
expectExpression(exp.subExpressions[1], secondType);
|
||||
}
|
||||
if (thirdType) {
|
||||
expectExpression(exp.subExpressions[2], thirdType);
|
||||
}
|
||||
if (fourthType) {
|
||||
expectExpression(exp.subExpressions[3], fourthType);
|
||||
}
|
||||
return [
|
||||
exp.subExpressions[0] as FirstT,
|
||||
exp.subExpressions[1] as SecondT,
|
||||
exp.subExpressions[2] as ThirdT,
|
||||
exp.subExpressions[3] as FourthT
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": ["./**/*.spec.ts"],
|
||||
"helpers": ["helpers/**/*.js"],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
"spec_dir": "",
|
||||
"spec_files": [
|
||||
"spec/**/*.spec.ts",
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"helpers": ["helpers/**/*.js"],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue