diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed49944d3d..070390da97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,9 @@ on: - "apps/simplex-badge-service/web/**" - "scripts/ci/packages_release.py" - "scripts/ci/test_packages_release.py" + - "website/src/js/simplex-lib.jsc" + - "website/test/**" + - "website/package.json" types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: inputs: diff --git a/scripts/ci/README.md b/scripts/ci/README.md index 50fad152fb..deb6967d38 100644 --- a/scripts/ci/README.md +++ b/scripts/ci/README.md @@ -115,13 +115,27 @@ The reduced build runs one Linux job, build-linux (ubuntu-22.04 x86_64), for eve - CI, in build-linux: `scope-tests.sh ci-scripts` (step "Test changed scopes"). - Local: `python3 scripts/ci/test_packages_release.py`; Python 3.11 or newer is required for tomllib (packages_release.py). +#### website + +- Globs: + - website/src/js/simplex-lib.jsc + - website/test/** + - website/package.json +- CI: + - in build-linux: `scope-tests.sh website` (step "Test changed scopes"); + - on master pushes: build and deploy of website/**, images/**, blog/**, docs/** and PRIVACY.md (web.yml). +- Local: + ```sh + cd website + npm test + ``` + #### Scopes lacking CI tests | Scope | Paths | CI | |---|---|---| | ios | apps/ios/** | compile only | | types package | packages/simplex-chat-client/types/typescript/** | compile only | -| website | website/**
images/**
blog/**
docs/**
PRIVACY.md | build and deploy on master pushes (web.yml) | | ts-client | packages/simplex-chat-client/typescript/** | - | | webrtc | packages/simplex-chat-webrtc/** | - | | mcp | packages/simplex-chat-mcp/** | - | diff --git a/scripts/ci/packages_release.py b/scripts/ci/packages_release.py index 7b8d5db551..663d4be8e1 100644 --- a/scripts/ci/packages_release.py +++ b/scripts/ci/packages_release.py @@ -82,6 +82,7 @@ BUILD_CHANGES = { SCOPE_CHANGES = { "badge-web": ["apps/simplex-badge-service/web/**"], "ci-scripts": ["scripts/ci/packages_release.py", "scripts/ci/test_packages_release.py"], + "website": ["website/src/js/simplex-lib.jsc", "website/test/**", "website/package.json"], } CABAL_FILE = "simplex-chat.cabal" CABAL_VERSION = re.compile(r"^(version:\s*)(\S+)", re.M) diff --git a/scripts/ci/scope-tests.sh b/scripts/ci/scope-tests.sh index fe4b2807a9..1a48857c37 100755 --- a/scripts/ci/scope-tests.sh +++ b/scripts/ci/scope-tests.sh @@ -11,8 +11,11 @@ case "${1:-}" in ci-scripts) python3 scripts/ci/test_packages_release.py ;; + website) + (cd website && npm test) + ;; *) - echo "usage: $0 badge-web | ci-scripts" >&2 + echo "usage: $0 badge-web | ci-scripts | website" >&2 exit 1 ;; esac diff --git a/scripts/ci/test_packages_release.py b/scripts/ci/test_packages_release.py index 72e4295a20..365fd5c7fa 100644 --- a/scripts/ci/test_packages_release.py +++ b/scripts/ci/test_packages_release.py @@ -514,6 +514,10 @@ class ChangesTest(GitRepoTest): self.assertEqual(self.builds("scripts/ci/packages_release.py"), set()) self.assertEqual(self.values["scopes"], "ci-scripts") + def test_website_library_change_tests_its_scope(self): + self.assertEqual(self.builds("website/src/js/simplex-lib.jsc"), set()) + self.assertEqual(self.values["scopes"], "website") + class PackageStateTest(GitRepoTest): def setUp(self): diff --git a/website/package.json b/website/package.json index 7aa8a1399e..f6bb64f15d 100644 --- a/website/package.json +++ b/website/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "npm run build:js && npm run build:eleventy && npm run build:tailwind", "start": "npx eleventy --serve", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node --test test/simplex-lib.test.js", "build:js": "cp ./node_modules/qrcode/build/qrcode.js ./src/js/ && cp ./node_modules/@plausible-analytics/tracker/plausible.js ./src/js/page.js && ./copy_call.sh", "build:eleventy": "eleventy", "build:tailwind": "npx tailwindcss -i tailwind.css -o _site/css/tailwind.css", diff --git a/website/src/js/channel-preview.jsc b/website/src/js/channel-preview.jsc index d754d36b9c..77260a22de 100644 --- a/website/src/js/channel-preview.jsc +++ b/website/src/js/channel-preview.jsc @@ -507,7 +507,6 @@ const STYLE = ` min-width: 0; } -.simplex-preview-text .md-gap { height: 0.7em; } .simplex-preview-text .md-h1 { font-size: 1.5em; line-height: 1.3; font-weight: 700; } .simplex-preview-text .md-h2 { font-size: 1.3em; line-height: 1.3; font-weight: 700; } .simplex-preview-text .md-h3 { font-size: 1.15em; line-height: 1.3; font-weight: 700; } @@ -927,7 +926,7 @@ function render(container, data, channelLink, showAppBadges) { var welcomeMsg = { sender: null, ts: messages.length > 0 ? messages[0].ts : new Date().toISOString(), - content: { type: 'text', text: typeof welcome === 'string' ? welcome : '' }, + content: { type: 'text', text: typeof welcome === 'string' ? welcome : channel.description }, formattedText: Array.isArray(welcome) ? welcome : null, markdownBlocks: data.welcomeMessageBlocks, reactions: [] @@ -1547,7 +1546,7 @@ function appendTextBlock(bubble, msg) { textDiv.className = 'simplex-preview-text'; const meta = renderMetaHTML(msg); if (msg.formattedText && msg.formattedText.length > 0) { - textDiv.innerHTML = renderMarkdown(msg.formattedText, msg.markdownBlocks) + meta; + textDiv.innerHTML = renderMarkdown(msg.formattedText, msg.markdownBlocks, msg.content.text) + meta; } else { textDiv.innerHTML = escapeHtml(msg.content.text || '') + meta; } diff --git a/website/src/js/simplex-lib.jsc b/website/src/js/simplex-lib.jsc index 73e304f9dd..197d2fe41c 100644 --- a/website/src/js/simplex-lib.jsc +++ b/website/src/js/simplex-lib.jsc @@ -58,6 +58,8 @@ const MAX_LIST_DEPTH = 7; const MAX_TABLE_CELLS = 1000; +const MAX_EMPTY_LINES = 100; + const COPY_BUTTON = '

')); + check( + [plain('a'), paragraphBreak(2), markup('rowMark', '| '), plain('x'), markup('cellMark', ' |'), hardBreak, markup('delimiterRow', '|---|', { columns: ['none'] }), paragraphBreak(3), plain('b')], + [paragraph(0, 1), { type: 'table', columns: ['none'], header: [range(3, 4)], rows: [] }, paragraph(8, 9)], + div('a') + br(1) + '
x
' + br(2) + 'b' + ); +}); + +test('empty lines in a quote are lines of its text', () => { + check( + [quoteMark('> '), plain('a'), hardBreak, quoteMark('>'), hardBreak, quoteMark('> '), plain('b')], + [{ type: 'quote', content: range(1, 7) }], + '
a

b
' + ); +}); + +test('at most 100 empty lines in a row', () => { + check([plain('a'), paragraphBreak(5000), plain('b')], [paragraph(0, 1), paragraph(2, 3)], div('a') + br(100) + 'b'); + check([paragraphBreak(5000), plain('a')], [paragraph(1, 2)], br(100) + 'a'); + check([plain('a'), paragraphBreak(5000)], [paragraph(0, 1)], div('a') + br(100)); + check( + [listMark('- '), plain('a'), paragraphBreak(5000), listMark('- '), plain('b')], + [list(entry(1, 2), entry(4, 5))], + li('a') + br(100) + li('b') + ); +}); + +test('plain text and inline formatting without blocks', () => { + assert.strictEqual(escapeHtml('a\n\n\nb'), 'a


b'); + check([{ format: { type: 'bold', singleStar: true }, text: 'a' }, plain('\nb')], undefined, 'a
b'); +}); + +test('invalid blocks are drawn as text', () => { + check([plain('a'), paragraphBreak(2), plain('b')], [{ type: 'unknown' }], 'a

b'); +}); + +test('text outside messages keeps one gap between blocks', () => { + const page = (fts, blocks, html) => assert.strictEqual(renderMarkdown(fts, blocks), html); + const gap = '
'; + const blocks = [heading(1, 2), paragraph(3, 4)]; + page([headingMark, plain('a'), lineBreak, plain('b')], blocks, h1('a') + div('b')); + page([headingMark, plain('a'), paragraphBreak(3), plain('b')], blocks, h1('a') + gap + div('b')); + page([plain('a'), paragraphBreak(2), listMark('- '), plain('b')], [paragraph(0, 1), list(entry(3, 4))], div('a') + gap + li('b')); + page([listMark('- '), plain('a'), paragraphBreak(2), listMark('- '), plain('b')], [list(entry(1, 2), entry(4, 5))], li('a') + li('b')); + page([paragraphBreak(2), plain('a'), paragraphBreak(2)], [paragraph(1, 2)], div('a')); +});