diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8daf2f468..a146d15a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: - master - tiddlywiki-com env: - NODE_VERSION: "12" + NODE_VERSION: "18" jobs: test: runs-on: ubuntu-latest @@ -14,7 +14,13 @@ jobs: - uses: actions/setup-node@v1 with: node-version: "${{ env.NODE_VERSION }}" - - run: "./bin/test.sh" + - run: "./bin/ci-test.sh" + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 build-prerelease: runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' diff --git a/.gitignore b/.gitignore index 351c576ad..0ab5b300f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ tmp/ output/ node_modules/ - +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/bin/ci-test.sh b/bin/ci-test.sh new file mode 100755 index 000000000..ffcae66b2 --- /dev/null +++ b/bin/ci-test.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# test TiddlyWiki5 for tiddlywiki.com + +node ./tiddlywiki.js \ + ./editions/test \ + --verbose \ + --version \ + --rendertiddler $:/core/save/all test.html text/plain \ + --test \ + || exit 1 + +npm install playwright @playwright/test +npx playwright install chromium firefox --with-deps + +npx playwright test diff --git a/core/modules/filters/listops.js b/core/modules/filters/listops.js index 02d92831a..89bd8eeb7 100644 --- a/core/modules/filters/listops.js +++ b/core/modules/filters/listops.js @@ -58,6 +58,7 @@ Last entry/entries in list exports.last = function(source,operator,options) { var count = $tw.utils.getInt(operator.operand,1), results = []; + if(count === 0) return results; source(function(tiddler,title) { results.push(title); }); diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index 21e87f30d..9fffb1714 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -823,8 +823,8 @@ exports.hashString = function(str) { Base64 utility functions that work in either browser or Node.js */ if(typeof window !== 'undefined') { - exports.btoa = window.btoa; - exports.atob = window.atob; + exports.btoa = function(binstr) { return window.btoa(binstr); } + exports.atob = function(b64) { return window.atob(b64); } } else { exports.btoa = function(binstr) { return Buffer.from(binstr, 'binary').toString('base64'); diff --git a/core/modules/widgets/list.js b/core/modules/widgets/list.js index 39c7e1b84..faedf72cc 100755 --- a/core/modules/widgets/list.js +++ b/core/modules/widgets/list.js @@ -28,6 +28,18 @@ Inherit from the base widget class */ ListWidget.prototype = new Widget(); +ListWidget.prototype.initialise = function(parseTreeNode,options) { + // Bail if parseTreeNode is undefined, meaning that the ListWidget constructor was called without any arguments so that it can be subclassed + if(parseTreeNode === undefined) { + return; + } + // First call parent constructor to set everything else up + Widget.prototype.initialise.call(this,parseTreeNode,options); + // Now look for <$list-template> and <$list-empty> widgets as immediate child widgets + // This is safe to do during initialization because parse trees never change after creation + this.findExplicitTemplates(); +} + /* Render this widget into the DOM */ @@ -68,8 +80,6 @@ ListWidget.prototype.execute = function() { this.counterName = this.getAttribute("counter"); this.storyViewName = this.getAttribute("storyview"); this.historyTitle = this.getAttribute("history"); - // Look for <$list-template> and <$list-empty> widgets as immediate child widgets - this.findExplicitTemplates(); // Compose the list elements this.list = this.getTiddlerList(); var members = [], @@ -92,6 +102,7 @@ ListWidget.prototype.findExplicitTemplates = function() { var self = this; this.explicitListTemplate = null; this.explicitEmptyTemplate = null; + this.hasTemplateInBody = false; var searchChildren = function(childNodes) { $tw.utils.each(childNodes,function(node) { if(node.type === "list-template") { @@ -100,6 +111,8 @@ ListWidget.prototype.findExplicitTemplates = function() { self.explicitEmptyTemplate = node.children; } else if(node.type === "element" && node.tag === "p") { searchChildren(node.children); + } else { + self.hasTemplateInBody = true; } }); }; @@ -160,11 +173,11 @@ ListWidget.prototype.makeItemTemplate = function(title,index) { // Check for a <$list-item> widget if(this.explicitListTemplate) { templateTree = this.explicitListTemplate; - } else if (!this.explicitEmptyTemplate) { + } else if(this.hasTemplateInBody) { templateTree = this.parseTreeNode.children; } } - if(!templateTree) { + if(!templateTree || templateTree.length === 0) { // Default template is a link to the title templateTree = [{type: "element", tag: this.parseTreeNode.isBlock ? "div" : "span", children: [{type: "link", attributes: {to: {type: "string", value: title}}, children: [ {type: "text", text: title} @@ -414,4 +427,27 @@ ListItemWidget.prototype.refresh = function(changedTiddlers) { exports.listitem = ListItemWidget; +/* +Make <$list-template> and <$list-empty> widgets that do nothing +*/ +var ListTemplateWidget = function(parseTreeNode,options) { + // Main initialisation inherited from widget.js + this.initialise(parseTreeNode,options); +}; +ListTemplateWidget.prototype = new Widget(); +ListTemplateWidget.prototype.render = function() {} +ListTemplateWidget.prototype.refresh = function() { return false; } + +exports["list-template"] = ListTemplateWidget; + +var ListEmptyWidget = function(parseTreeNode,options) { + // Main initialisation inherited from widget.js + this.initialise(parseTreeNode,options); +}; +ListEmptyWidget.prototype = new Widget(); +ListEmptyWidget.prototype.render = function() {} +ListEmptyWidget.prototype.refresh = function() { return false; } + +exports["list-empty"] = ListEmptyWidget; + })(); diff --git a/core/ui/EditTemplate/fields.tid b/core/ui/EditTemplate/fields.tid index 6a767517b..e4381cbe7 100644 --- a/core/ui/EditTemplate/fields.tid +++ b/core/ui/EditTemplate/fields.tid @@ -89,7 +89,7 @@ $value={{{ [subfilterget[text]] }}}/> <$button class="tc-btn-invisible" tooltip={{$:/language/EditTemplate/Field/Remove/Hint}} aria-label={{$:/language/EditTemplate/Field/Remove/Caption}}> -<$action-deletefield $field=<>/><$set name="currentTiddlerCSSescaped" value={{{ [escapecss[]] }}}><$action-sendmessage $message="tm-focus-selector" $param=<>/> +<$action-deletefield $field=<>/> {{$:/core/images/delete-button}} diff --git a/core/wiki/macros/toc.tid b/core/wiki/macros/toc.tid index a925e7ee5..a3f5b002a 100644 --- a/core/wiki/macros/toc.tid +++ b/core/wiki/macros/toc.tid @@ -118,7 +118,7 @@ tags: $:/tags/Macro <$set name="toc-item-class" filter=<<__itemClassFilter__>> emptyValue="toc-item-selected" value="toc-item" >
  • >> <$link to={{{ [get[target]else] }}}> - <$list filter="[all[current]tagging[]$sort$limit[1]]" variable="ignore" emptyMessage="<$button class='tc-btn-invisible'>{{$:/core/images/blank}}"> + <$list filter="[all[current]tagging[]$sort$limit[1]] -[subfilter<__exclude__>]" variable="ignore" emptyMessage="<$button class='tc-btn-invisible'>{{$:/core/images/blank}}"> <$reveal type="nomatch" stateTitle=<> text="open"> <$button setTitle=<> setTo="open" class="tc-btn-invisible tc-popup-keep"> <$transclude tiddler=<> /> @@ -145,7 +145,7 @@ tags: $:/tags/Macro <$qualify name="toc-state" title={{{ [[$:/state/toc]addsuffix<__path__>addsuffix[-]addsuffix] }}}> <$set name="toc-item-class" filter=<<__itemClassFilter__>> emptyValue="toc-item-selected" value="toc-item">
  • >> - <$list filter="[all[current]tagging[]$sort$limit[1]]" variable="ignore" emptyMessage="""<$button class="tc-btn-invisible">{{$:/core/images/blank}}<>"""> + <$list filter="[all[current]tagging[]$sort$limit[1]] -[subfilter<__exclude__>]" variable="ignore" emptyMessage="""<$button class="tc-btn-invisible">{{$:/core/images/blank}}<>"""> <$reveal type="nomatch" stateTitle=<> text="open"> <$button setTitle=<> setTo="open" class="tc-btn-invisible tc-popup-keep"> <$transclude tiddler=<> /> diff --git a/editions/test/playwright.spec.js b/editions/test/playwright.spec.js new file mode 100644 index 000000000..1d8c624c7 --- /dev/null +++ b/editions/test/playwright.spec.js @@ -0,0 +1,25 @@ +const { test, expect } = require('@playwright/test'); +const {resolve} = require('path'); + +const indexPath = resolve(__dirname, 'output', 'test.html'); +const crossPlatformIndexPath = indexPath.replace(/^\/+/, ''); + + +test('get started link', async ({ page }) => { + // The tests can take a while to run + const timeout = 1000 * 30; + test.setTimeout(timeout); + + // Load the generated test TW html + await page.goto(`file:///${crossPlatformIndexPath}`); + + // Sanity check + await expect(page.locator('.tc-site-title'), "Expected correct page title to verify the test page was loaded").toHaveText('TiddlyWiki5'); + + // Wait for jasmine results bar to appear + await expect(page.locator('.jasmine-overall-result'), "Expected jasmine test results bar to be present").toBeVisible({timeout}); + + // Assert the tests have passed + await expect(page.locator('.jasmine-overall-result.jasmine-failed'), "Expected jasmine tests to not have failed").not.toBeVisible(); + await expect(page.locator('.jasmine-overall-result.jasmine-passed'), "Expected jasmine tests to have passed").toBeVisible(); +}); diff --git a/editions/test/tiddlers/tests/test-filters.js b/editions/test/tiddlers/tests/test-filters.js index 9e2f53b1a..727f64ca4 100644 --- a/editions/test/tiddlers/tests/test-filters.js +++ b/editions/test/tiddlers/tests/test-filters.js @@ -365,6 +365,7 @@ Tests the filtering mechanism. expect(wiki.filterTiddlers("[sort[title]first[8]]").join(",")).toBe("$:/ShadowPlugin,$:/TiddlerTwo,a fourth tiddler,filter regexp test,has filter,hasList,one,Tiddler Three"); expect(wiki.filterTiddlers("[sort[title]first[x]]").join(",")).toBe("$:/ShadowPlugin"); expect(wiki.filterTiddlers("[sort[title]last[]]").join(",")).toBe("TiddlerOne"); + expect(wiki.filterTiddlers("[sort[title]last[0]]").join(",")).toBe(""); expect(wiki.filterTiddlers("[sort[title]last[2]]").join(",")).toBe("Tiddler Three,TiddlerOne"); expect(wiki.filterTiddlers("[sort[title]last[8]]").join(",")).toBe("$:/TiddlerTwo,a fourth tiddler,filter regexp test,has filter,hasList,one,Tiddler Three,TiddlerOne"); expect(wiki.filterTiddlers("[sort[title]last[x]]").join(",")).toBe("TiddlerOne"); diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..491679a6f --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,46 @@ +const { defineConfig, devices } = require('@playwright/test'); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: './editions/test/', + + // Allow parallel tests + fullyParallel: true, + + // Prevent accidentally committed "test.only" from wrecking havoc + forbidOnly: !!process.env.CI, + + // Do not retry tests on failure + retries: 0, + + // How many parallel workers + workers: process.env.CI ? 1 : undefined, + + // Reporter to use. See https://playwright.dev/docs/test-reporters + reporter: 'html', + + // Settings shared with all the tests + use: { + // Take a screenshot when the test fails + screenshot: { + mode: 'only-on-failure', + fullPage: true + } + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + } + ], +}); + diff --git a/plugins/tiddlywiki/codemirror/styles.tid b/plugins/tiddlywiki/codemirror/styles.tid index a070a614d..d0083805d 100755 --- a/plugins/tiddlywiki/codemirror/styles.tid +++ b/plugins/tiddlywiki/codemirror/styles.tid @@ -56,6 +56,11 @@ name: tiddlywiki rendering-intent: auto; } +.tc-tiddler-frame .tc-tiddler-editor .tc-edit-texteditor, +.tc-tiddler-frame .tc-tiddler-editor .tc-tiddler-preview-preview { + overflow: auto; +} + .cm-s-tiddlywiki.CodeMirror, .cm-s-tiddlywiki .CodeMirror-gutters { background-color: <>; color: <>; } .cm-s-tiddlywiki .CodeMirror-gutters {background: <>; border-right: 1px solid <>;} .cm-s-tiddlywiki .CodeMirror-linenumber {color: <>;}