Add Selenium integration test framework

This commit is contained in:
gsb 2026-04-29 18:04:20 +00:00
parent 1198791505
commit 3e8d3388f6
12 changed files with 703 additions and 7 deletions

View File

@ -157,4 +157,4 @@ def broadcast_peers():
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, port=5000) app.run(debug=True, host="0.0.0.0", port=5000)

View File

@ -0,0 +1 @@
/tmp/ribbit/dist/ribbit

View File

@ -11,6 +11,16 @@
#status { font-size: 12px; color: #666; margin-bottom: 10px; } #status { font-size: 12px; color: #666; margin-bottom: 10px; }
#revisions { margin-top: 20px; } #revisions { margin-top: 20px; }
#revisions button { margin: 2px; } #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> </style>
</head> </head>
<body> <body>

View File

@ -3,6 +3,7 @@ module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
testEnvironment: 'node', testEnvironment: 'node',
roots: ['<rootDir>/test'], roots: ['<rootDir>/test'],
testPathIgnorePatterns: ['/node_modules/', '/test/integration/'],
moduleNameMapper: { moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1', '^(\\.{1,2}/.*)\\.js$': '$1',
}, },

279
package-lock.json generated
View File

@ -13,6 +13,7 @@
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"happy-dom": "^14.12.3", "happy-dom": "^14.12.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"selenium-webdriver": "^4.43.0",
"ts-jest": "^29.4.9", "ts-jest": "^29.4.9",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }
@ -472,6 +473,12 @@
"node": ">=6.9.0" "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": { "node_modules/@bcoe/v8-coverage": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
@ -1795,6 +1802,12 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true "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": { "node_modules/create-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@ -2268,6 +2281,12 @@
"node": ">=10.17.0" "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": { "node_modules/import-local": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
@ -2373,6 +2392,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -3073,6 +3098,18 @@
"node": ">=6" "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": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -3091,6 +3128,15 @@
"node": ">=6" "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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -3341,6 +3387,12 @@
"node": ">=6" "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": { "node_modules/parse-json": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "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" "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": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -3492,6 +3550,21 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true "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": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -3552,6 +3625,37 @@
"node": ">=10" "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": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -3561,6 +3665,12 @@
"semver": "bin/semver.js" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3640,6 +3750,15 @@
"node": ">=10" "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": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -3747,6 +3866,15 @@
"node": ">=8" "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": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -3924,6 +4052,12 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/v8-to-istanbul": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "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": "^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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -4403,6 +4558,12 @@
"@babel/helper-validator-identifier": "^7.28.5" "@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": { "@bcoe/v8-coverage": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
@ -5305,6 +5466,12 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true "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": { "create-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@ -5640,6 +5807,12 @@
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true "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": { "import-local": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
@ -5711,6 +5884,12 @@
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true "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": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -6244,6 +6423,18 @@
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true "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": { "kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -6256,6 +6447,15 @@
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true "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": { "lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "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==", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true "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": { "parse-json": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "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": { "prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -6557,6 +6769,21 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true "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": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -6596,12 +6823,36 @@
"integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==",
"dev": true "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": { "semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true "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": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -6666,6 +6917,15 @@
"escape-string-regexp": "^2.0.0" "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": { "string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -6740,6 +7000,12 @@
"minimatch": "^3.0.4" "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": { "tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -6827,6 +7093,12 @@
"picocolors": "^1.1.1" "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": { "v8-to-istanbul": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@ -6901,6 +7173,13 @@
"signal-exit": "^3.0.7" "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": { "y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -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: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/", "build:css": "cp src/static/ribbit-core.css dist/ribbit/ && cp -r src/static/themes dist/ribbit/",
"test": "npm run build && jest --verbose", "test": "npm run build && jest --verbose",
"test:integration": "npm run build && node test/integration/test.js",
"test:coverage": "npm run build && jest --coverage" "test:coverage": "npm run build && jest --coverage"
}, },
"license": "MIT", "license": "MIT",
@ -24,6 +25,7 @@
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"happy-dom": "^14.12.3", "happy-dom": "^14.12.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"selenium-webdriver": "^4.43.0",
"ts-jest": "^29.4.9", "ts-jest": "^29.4.9",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }

View File

@ -52,6 +52,7 @@ export class RibbitEditor extends Ribbit {
this.element.parentNode?.insertBefore(this.toolbar.render(), this.element); this.element.parentNode?.insertBefore(this.toolbar.render(), this.element);
} }
this.view(); this.view();
this.emitReady();
} }
#bindEvents(): void { #bindEvents(): void {

View File

@ -146,12 +146,7 @@ export class Ribbit {
this.emitter.off(event, callback); this.emitter.off(event, callback);
} }
run(): void { protected emitReady(): void {
this.element.classList.add('loaded');
if (this.autoToolbar) {
this.element.parentNode?.insertBefore(this.toolbar.render(), this.element);
}
this.view();
this.emitter.emit('ready', { this.emitter.emit('ready', {
markdown: this.getMarkdown(), markdown: this.getMarkdown(),
html: this.getHTML(), 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 { getState(): string | null {
return this.state; return this.state;
} }

View File

@ -281,6 +281,7 @@ export class ToolbarManager {
const li = document.createElement('li'); const li = document.createElement('li');
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = `ribbit-btn-${button.id}`; btn.className = `ribbit-btn-${button.id}`;
btn.textContent = button.label;
btn.setAttribute('aria-label', button.label); btn.setAttribute('aria-label', button.label);
btn.title = button.shortcut btn.title = button.shortcut
? `${button.label} (${button.shortcut})` ? `${button.label} (${button.shortcut})`
@ -298,6 +299,7 @@ export class ToolbarManager {
const li = document.createElement('li'); const li = document.createElement('li');
const toggle = document.createElement('button'); const toggle = document.createElement('button');
toggle.className = 'ribbit-btn-group'; toggle.className = 'ribbit-btn-group';
toggle.textContent = group.label + ' ▾';
toggle.setAttribute('aria-label', group.label); toggle.setAttribute('aria-label', group.label);
toggle.title = group.label; toggle.title = group.label;

View 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>

View 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
View 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);
}
})();