Add Selenium integration test framework
This commit is contained in:
parent
1198791505
commit
3e8d3388f6
|
|
@ -157,4 +157,4 @@ def broadcast_peers():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, port=5000)
|
||||
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||
|
|
|
|||
1
examples/flask-collab/static/ribbit
Symbolic link
1
examples/flask-collab/static/ribbit
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/tmp/ribbit/dist/ribbit
|
||||
|
|
@ -11,6 +11,16 @@
|
|||
#status { font-size: 12px; color: #666; margin-bottom: 10px; }
|
||||
#revisions { margin-top: 20px; }
|
||||
#revisions button { margin: 2px; }
|
||||
#ribbit { border: 1px solid #ccc; border-radius: 4px; padding: 20px; min-height: 200px; }
|
||||
.ribbit-toolbar { background: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 4px; margin-bottom: 8px; }
|
||||
.ribbit-toolbar ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 2px; align-items: center; }
|
||||
.ribbit-toolbar button { padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; font-size: 12px; }
|
||||
.ribbit-toolbar button:hover { background: #e8e8e8; }
|
||||
.ribbit-toolbar button.active { background: #d0d0ff; border-color: #99f; }
|
||||
.ribbit-toolbar button.disabled { opacity: 0.3; cursor: default; }
|
||||
.ribbit-toolbar .spacer { width: 12px; }
|
||||
.ribbit-dropdown { position: absolute; background: white; border: 1px solid #ccc; border-radius: 4px; padding: 4px; z-index: 10; }
|
||||
.ribbit-dropdown button { display: block; width: 100%; text-align: left; margin: 1px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ module.exports = {
|
|||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/test'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/test/integration/'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
|
|
|
|||
279
package-lock.json
generated
279
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"esbuild": "^0.28.0",
|
||||
"happy-dom": "^14.12.3",
|
||||
"jest": "^29.7.0",
|
||||
"selenium-webdriver": "^4.43.0",
|
||||
"ts-jest": "^29.4.9",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
|
|
@ -472,6 +473,12 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bazel/runfiles": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz",
|
||||
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
|
|
@ -1795,6 +1802,12 @@
|
|||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
||||
|
|
@ -2268,6 +2281,12 @@
|
|||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
|
||||
|
|
@ -2373,6 +2392,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -3073,6 +3098,18 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
|
|
@ -3091,6 +3128,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
|
|
@ -3341,6 +3387,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
|
|
@ -3457,6 +3509,12 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
|
|
@ -3492,6 +3550,21 @@
|
|||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -3552,6 +3625,37 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/selenium-webdriver": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.43.0.tgz",
|
||||
"integrity": "sha512-dV4zBTT37or3Z3/8uD6rS8zvd4ZxPuG4EJVlqYIbZCGZCYttZm7xb9rlFLSk4rrsQHAeDYvudl7cquo0vWpHjg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/SeleniumHQ"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/selenium"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@bazel/runfiles": "^6.5.0",
|
||||
"jszip": "^3.10.1",
|
||||
"tmp": "^0.2.5",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
|
|
@ -3561,6 +3665,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -3640,6 +3750,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-length": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||
|
|
@ -3747,6 +3866,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
|
|
@ -3924,6 +4052,12 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||
|
|
@ -4022,6 +4156,27 @@
|
|||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
|
@ -4403,6 +4558,12 @@
|
|||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
}
|
||||
},
|
||||
"@bazel/runfiles": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz",
|
||||
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
|
||||
"dev": true
|
||||
},
|
||||
"@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
|
|
@ -5305,6 +5466,12 @@
|
|||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"create-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
||||
|
|
@ -5640,6 +5807,12 @@
|
|||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true
|
||||
},
|
||||
"immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"import-local": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
|
||||
|
|
@ -5711,6 +5884,12 @@
|
|||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -6244,6 +6423,18 @@
|
|||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true
|
||||
},
|
||||
"jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
|
|
@ -6256,6 +6447,15 @@
|
|||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
|
||||
"dev": true
|
||||
},
|
||||
"lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
|
|
@ -6453,6 +6653,12 @@
|
|||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
},
|
||||
"pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"dev": true
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
|
|
@ -6535,6 +6741,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
|
|
@ -6557,6 +6769,21 @@
|
|||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -6596,12 +6823,36 @@
|
|||
"integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
|
||||
"dev": true
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"selenium-webdriver": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.43.0.tgz",
|
||||
"integrity": "sha512-dV4zBTT37or3Z3/8uD6rS8zvd4ZxPuG4EJVlqYIbZCGZCYttZm7xb9rlFLSk4rrsQHAeDYvudl7cquo0vWpHjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@bazel/runfiles": "^6.5.0",
|
||||
"jszip": "^3.10.1",
|
||||
"tmp": "^0.2.5",
|
||||
"ws": "^8.20.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true
|
||||
},
|
||||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"dev": true
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -6666,6 +6917,15 @@
|
|||
"escape-string-regexp": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"string-length": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||
|
|
@ -6740,6 +7000,12 @@
|
|||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true
|
||||
},
|
||||
"tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
|
|
@ -6827,6 +7093,12 @@
|
|||
"picocolors": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||
|
|
@ -6901,6 +7173,13 @@
|
|||
"signal-exit": "^3.0.7"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"build:core-min": "esbuild src/ts/ribbit-core.ts --bundle --format=iife --global-name=ribbit --minify --outfile=dist/ribbit/ribbit-core.min.js",
|
||||
"build:css": "cp src/static/ribbit-core.css dist/ribbit/ && cp -r src/static/themes dist/ribbit/",
|
||||
"test": "npm run build && jest --verbose",
|
||||
"test:integration": "npm run build && node test/integration/test.js",
|
||||
"test:coverage": "npm run build && jest --coverage"
|
||||
},
|
||||
"license": "MIT",
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
"esbuild": "^0.28.0",
|
||||
"happy-dom": "^14.12.3",
|
||||
"jest": "^29.7.0",
|
||||
"selenium-webdriver": "^4.43.0",
|
||||
"ts-jest": "^29.4.9",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class RibbitEditor extends Ribbit {
|
|||
this.element.parentNode?.insertBefore(this.toolbar.render(), this.element);
|
||||
}
|
||||
this.view();
|
||||
this.emitReady();
|
||||
}
|
||||
|
||||
#bindEvents(): void {
|
||||
|
|
|
|||
|
|
@ -146,12 +146,7 @@ export class Ribbit {
|
|||
this.emitter.off(event, callback);
|
||||
}
|
||||
|
||||
run(): void {
|
||||
this.element.classList.add('loaded');
|
||||
if (this.autoToolbar) {
|
||||
this.element.parentNode?.insertBefore(this.toolbar.render(), this.element);
|
||||
}
|
||||
this.view();
|
||||
protected emitReady(): void {
|
||||
this.emitter.emit('ready', {
|
||||
markdown: this.getMarkdown(),
|
||||
html: this.getHTML(),
|
||||
|
|
@ -160,6 +155,15 @@ export class Ribbit {
|
|||
});
|
||||
}
|
||||
|
||||
run(): void {
|
||||
this.element.classList.add('loaded');
|
||||
if (this.autoToolbar) {
|
||||
this.element.parentNode?.insertBefore(this.toolbar.render(), this.element);
|
||||
}
|
||||
this.view();
|
||||
this.emitReady();
|
||||
}
|
||||
|
||||
getState(): string | null {
|
||||
return this.state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,6 +281,7 @@ export class ToolbarManager {
|
|||
const li = document.createElement('li');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `ribbit-btn-${button.id}`;
|
||||
btn.textContent = button.label;
|
||||
btn.setAttribute('aria-label', button.label);
|
||||
btn.title = button.shortcut
|
||||
? `${button.label} (${button.shortcut})`
|
||||
|
|
@ -298,6 +299,7 @@ export class ToolbarManager {
|
|||
const li = document.createElement('li');
|
||||
const toggle = document.createElement('button');
|
||||
toggle.className = 'ribbit-btn-group';
|
||||
toggle.textContent = group.label + ' ▾';
|
||||
toggle.setAttribute('aria-label', group.label);
|
||||
toggle.title = group.label;
|
||||
|
||||
|
|
|
|||
46
test/integration/index.html
Normal file
46
test/integration/index.html
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ribbit Integration Test Page</title>
|
||||
<link rel="stylesheet" href="/ribbit/themes/ribbit-default/theme.css">
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
#ribbit { border: 1px solid #ccc; padding: 20px; min-height: 200px; }
|
||||
.ribbit-toolbar { background: #f5f5f5; border: 1px solid #ccc; padding: 4px; margin-bottom: 8px; }
|
||||
.ribbit-toolbar ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 2px; }
|
||||
.ribbit-toolbar button { padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; font-size: 12px; }
|
||||
.ribbit-toolbar button.active { background: #d0d0ff; }
|
||||
.ribbit-toolbar button.disabled { opacity: 0.3; }
|
||||
.ribbit-toolbar .spacer { width: 12px; }
|
||||
.ribbit-dropdown { position: absolute; background: white; border: 1px solid #ccc; padding: 4px; }
|
||||
.ribbit-dropdown button { display: block; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article id="ribbit">**bold** and *italic* and `code`
|
||||
|
||||
## Heading
|
||||
|
||||
- list item 1
|
||||
- list item 2
|
||||
|
||||
> a blockquote
|
||||
|
||||
| A | B |
|
||||
|---|---|
|
||||
| 1 | 2 |
|
||||
</article>
|
||||
|
||||
<script src="/ribbit/ribbit.js"></script>
|
||||
<script>
|
||||
const editor = new ribbit.Editor({
|
||||
on: {
|
||||
ready: () => { window.__ribbitReady = true; },
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
window.__ribbitEditor = editor;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
60
test/integration/server.js
Normal file
60
test/integration/server.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Minimal static file server for e2e tests.
|
||||
* Serves the test page and ribbit dist files.
|
||||
*/
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.map': 'application/json',
|
||||
};
|
||||
|
||||
function createServer(port = 9999) {
|
||||
const distDir = path.join(__dirname, '..', '..', 'dist', 'ribbit');
|
||||
const testDir = __dirname;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
let filePath;
|
||||
if (req.url === '/' || req.url === '/index.html') {
|
||||
filePath = path.join(testDir, 'index.html');
|
||||
} else if (req.url.startsWith('/ribbit/')) {
|
||||
filePath = path.join(distDir, req.url.replace('/ribbit/', ''));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
const mime = MIME[ext] || 'application/octet-stream';
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath);
|
||||
res.writeHead(200, { 'Content-Type': mime });
|
||||
res.end(content);
|
||||
} catch {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
start() {
|
||||
return new Promise((resolve) => {
|
||||
server.listen(port, () => resolve());
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
return new Promise((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
},
|
||||
url: `http://localhost:${port}`,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createServer };
|
||||
290
test/integration/test.js
Normal file
290
test/integration/test.js
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
/**
|
||||
* Integration tests for the ribbit editor using Selenium + Firefox.
|
||||
*
|
||||
* Run: npm run test:e2e
|
||||
*/
|
||||
const { Builder, By, Key, until } = require('selenium-webdriver');
|
||||
const firefox = require('selenium-webdriver/firefox');
|
||||
const { createServer } = require('./server');
|
||||
|
||||
let server;
|
||||
let driver;
|
||||
|
||||
async function setup() {
|
||||
server = createServer(9999);
|
||||
await server.start();
|
||||
|
||||
const options = new firefox.Options().addArguments('--headless');
|
||||
driver = await new Builder()
|
||||
.forBrowser('firefox')
|
||||
.setFirefoxOptions(options)
|
||||
.build();
|
||||
|
||||
await driver.get(server.url);
|
||||
// Wait for ribbit to initialize
|
||||
await driver.wait(async () => {
|
||||
return driver.executeScript('return window.__ribbitReady === true');
|
||||
}, 10000).catch(async () => {
|
||||
const logs = await driver.manage().logs().get('browser').catch(() => []);
|
||||
console.log('Browser logs:', logs.map(l => l.message));
|
||||
const ready = await driver.executeScript('return { ready: window.__ribbitReady, ribbit: typeof window.ribbit, editor: typeof window.__ribbitEditor }');
|
||||
console.log('State:', ready);
|
||||
throw new Error('Editor did not become ready');
|
||||
});
|
||||
}
|
||||
|
||||
async function teardown() {
|
||||
if (driver) await driver.quit();
|
||||
if (server) await server.stop();
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
async function getEditorHTML() {
|
||||
return driver.executeScript('return document.getElementById("ribbit").innerHTML');
|
||||
}
|
||||
|
||||
async function getEditorText() {
|
||||
return driver.executeScript('return document.getElementById("ribbit").textContent');
|
||||
}
|
||||
|
||||
async function getState() {
|
||||
return driver.executeScript('return window.__ribbitEditor.getState()');
|
||||
}
|
||||
|
||||
async function clickButton(label) {
|
||||
const buttons = await driver.findElements(By.css('.ribbit-toolbar button'));
|
||||
for (const btn of buttons) {
|
||||
const text = await btn.getText();
|
||||
if (text === label) {
|
||||
await btn.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(`Button "${label}" not found`);
|
||||
}
|
||||
|
||||
async function clickEditor() {
|
||||
const editor = await driver.findElement(By.id('ribbit'));
|
||||
await editor.click();
|
||||
}
|
||||
|
||||
// Test runner
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
async function test(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
passed++;
|
||||
console.log(` ✓ ${name}`);
|
||||
} catch (e) {
|
||||
failed++;
|
||||
errors.push(name);
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
|
||||
// Tests
|
||||
async function runTests() {
|
||||
console.log('\nRibbit Integration Tests\n');
|
||||
|
||||
await test('page loads', async () => {
|
||||
const title = await driver.getTitle();
|
||||
assert(title === 'Ribbit Integration Test Page', `Title: ${title}`);
|
||||
});
|
||||
|
||||
await test('editor renders in view mode', async () => {
|
||||
const state = await getState();
|
||||
assert(state === 'view', `State: ${state}`);
|
||||
});
|
||||
|
||||
await test('editor renders markdown as HTML', async () => {
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('<strong>bold</strong>'), 'Missing bold');
|
||||
assert(html.includes('<em>italic</em>'), 'Missing italic');
|
||||
assert(html.includes('<code>code</code>'), 'Missing code');
|
||||
});
|
||||
|
||||
await test('editor renders headings', async () => {
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('<h2'), 'Missing h2');
|
||||
});
|
||||
|
||||
await test('editor renders lists', async () => {
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('<ul>'), 'Missing ul');
|
||||
assert(html.includes('<li>'), 'Missing li');
|
||||
});
|
||||
|
||||
await test('editor renders tables', async () => {
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('<table>'), 'Missing table');
|
||||
});
|
||||
|
||||
await test('editor renders blockquotes', async () => {
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('<blockquote>'), 'Missing blockquote');
|
||||
});
|
||||
|
||||
await test('toolbar is rendered', async () => {
|
||||
const toolbar = await driver.findElements(By.css('.ribbit-toolbar'));
|
||||
assert(toolbar.length > 0, 'No toolbar found');
|
||||
});
|
||||
|
||||
await test('toolbar has buttons with labels', async () => {
|
||||
const buttons = await driver.findElements(By.css('.ribbit-toolbar button'));
|
||||
assert(buttons.length > 5, `Only ${buttons.length} buttons`);
|
||||
const text = await buttons[0].getText();
|
||||
assert(text.length > 0, 'Button has no label');
|
||||
});
|
||||
|
||||
await test('toggle button switches to wysiwyg', async () => {
|
||||
await clickButton('Edit');
|
||||
const state = await getState();
|
||||
assert(state === 'wysiwyg', `State: ${state}`);
|
||||
});
|
||||
|
||||
await test('editor is contentEditable in wysiwyg', async () => {
|
||||
const editable = await driver.executeScript(
|
||||
'return document.getElementById("ribbit").contentEditable'
|
||||
);
|
||||
assert(editable === 'true', `contentEditable: ${editable}`);
|
||||
});
|
||||
|
||||
await test('can type in wysiwyg mode', async () => {
|
||||
await clickEditor();
|
||||
// Move to end and type
|
||||
await driver.actions().keyDown(Key.CONTROL).sendKeys(Key.END).keyUp(Key.CONTROL).perform();
|
||||
await driver.actions().sendKeys('\nhello from selenium').perform();
|
||||
const text = await getEditorText();
|
||||
assert(text.includes('hello from selenium'), 'Typed text not found');
|
||||
});
|
||||
|
||||
await test('source button switches to edit mode', async () => {
|
||||
await clickButton('Source');
|
||||
const state = await getState();
|
||||
assert(state === 'edit', `State: ${state}`);
|
||||
});
|
||||
|
||||
await test('edit mode shows raw markdown', async () => {
|
||||
const text = await getEditorText();
|
||||
assert(text.includes('**bold**'), 'Missing raw markdown');
|
||||
});
|
||||
|
||||
await test('toggle back to view mode', async () => {
|
||||
await clickButton('Edit');
|
||||
const state = await getState();
|
||||
assert(state === 'view', `State: ${state}`);
|
||||
});
|
||||
|
||||
await test('view mode renders HTML again', async () => {
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('<strong>bold</strong>'), 'Not rendered as HTML');
|
||||
});
|
||||
|
||||
await test('save button fires save event', async () => {
|
||||
await driver.executeScript('window.__saved = false; window.__ribbitEditor.on("save", () => { window.__saved = true; })');
|
||||
await clickButton('Edit');
|
||||
await clickButton('Save');
|
||||
const saved = await driver.executeScript('return window.__saved');
|
||||
assert(saved === true, 'Save event not fired');
|
||||
});
|
||||
|
||||
await test('enter key creates new line in wysiwyg', async () => {
|
||||
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
|
||||
await clickEditor();
|
||||
// Clear and type two lines
|
||||
await driver.actions().keyDown(Key.CONTROL).sendKeys('a').keyUp(Key.CONTROL).perform();
|
||||
await driver.actions().sendKeys(Key.DELETE).perform();
|
||||
await driver.actions().sendKeys('line one').perform();
|
||||
await driver.actions().sendKeys(Key.ENTER).perform();
|
||||
await driver.actions().sendKeys('line two').perform();
|
||||
const text = await getEditorText();
|
||||
assert(text.includes('line one'), `Missing "line one" in: ${text}`);
|
||||
assert(text.includes('line two'), `Missing "line two" in: ${text}`);
|
||||
// Check that they're on separate lines (not concatenated)
|
||||
const html = await getEditorHTML();
|
||||
const hasBreak = html.includes('<br') || html.includes('<div') || html.includes('<p');
|
||||
assert(hasBreak, `No line break in HTML: ${html}`);
|
||||
});
|
||||
|
||||
await test('enter key in wysiwyg produces valid markdown', async () => {
|
||||
// Get the markdown from the content typed above
|
||||
const md = await driver.executeScript('return window.__ribbitEditor.getMarkdown()');
|
||||
assert(md.includes('line one'), `Missing "line one" in markdown: ${md}`);
|
||||
assert(md.includes('line two'), `Missing "line two" in markdown: ${md}`);
|
||||
// Lines should be separate (not on same line)
|
||||
const lines = md.split('\n').filter(l => l.trim());
|
||||
const hasLineOne = lines.some(l => l.includes('line one'));
|
||||
const hasLineTwo = lines.some(l => l.includes('line two'));
|
||||
assert(hasLineOne, `"line one" not on its own line in: ${md}`);
|
||||
assert(hasLineTwo, `"line two" not on its own line in: ${md}`);
|
||||
});
|
||||
|
||||
await test('multiple enters create blank lines in wysiwyg', async () => {
|
||||
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
|
||||
await clickEditor();
|
||||
await driver.actions().keyDown(Key.CONTROL).sendKeys('a').keyUp(Key.CONTROL).perform();
|
||||
await driver.actions().sendKeys(Key.DELETE).perform();
|
||||
await driver.actions().sendKeys('para one').perform();
|
||||
await driver.actions().sendKeys(Key.ENTER, Key.ENTER).perform();
|
||||
await driver.actions().sendKeys('para two').perform();
|
||||
const text = await getEditorText();
|
||||
assert(text.includes('para one'), `Missing "para one" in: ${text}`);
|
||||
assert(text.includes('para two'), `Missing "para two" in: ${text}`);
|
||||
});
|
||||
|
||||
await test('enter after heading in wysiwyg', async () => {
|
||||
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
|
||||
await clickEditor();
|
||||
await driver.actions().keyDown(Key.CONTROL).sendKeys('a').keyUp(Key.CONTROL).perform();
|
||||
await driver.actions().sendKeys(Key.DELETE).perform();
|
||||
await driver.actions().sendKeys('## My Heading').perform();
|
||||
await driver.actions().sendKeys(Key.ENTER).perform();
|
||||
await driver.actions().sendKeys('paragraph text').perform();
|
||||
const md = await driver.executeScript('return window.__ribbitEditor.getMarkdown()');
|
||||
assert(md.includes('Heading') || md.includes('heading'), `Missing heading in: ${md}`);
|
||||
assert(md.includes('paragraph'), `Missing paragraph in: ${md}`);
|
||||
});
|
||||
|
||||
await test('Ctrl+B shortcut works in wysiwyg', async () => {
|
||||
// Switch to wysiwyg
|
||||
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
|
||||
await clickEditor();
|
||||
// Type and select
|
||||
await driver.actions().sendKeys('test text').perform();
|
||||
await driver.actions()
|
||||
.keyDown(Key.SHIFT)
|
||||
.sendKeys(Key.ARROW_LEFT, Key.ARROW_LEFT, Key.ARROW_LEFT, Key.ARROW_LEFT)
|
||||
.keyUp(Key.SHIFT)
|
||||
.perform();
|
||||
// Ctrl+B
|
||||
await driver.actions().keyDown(Key.CONTROL).sendKeys('b').keyUp(Key.CONTROL).perform();
|
||||
const html = await getEditorHTML();
|
||||
assert(html.includes('**'), 'Bold delimiter not inserted');
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await setup();
|
||||
await runTests();
|
||||
} catch (e) {
|
||||
console.error('Setup failed:', e.message);
|
||||
failed++;
|
||||
} finally {
|
||||
console.log(`\n${passed}/${passed + failed} passed — ${failed} failed`);
|
||||
if (errors.length) {
|
||||
console.log('\nFailed:');
|
||||
errors.forEach(e => console.log(` • ${e}`));
|
||||
}
|
||||
await teardown();
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user