Compare commits

...

313 commits

Author SHA1 Message Date
Camillo Bruni
5936c53eda Update leakIframes URL param when toggling state in dev menu
Change-Id: I5cc1ac948ec73f31973d9aa5b67087392424fc3c
2025-03-06 16:13:35 +01:00
Camillo Bruni
f227c26c11 Add params.leakyIframes to main-custom
DevTools can't handle sources from detached iframes.
To simplify local development we add an an option that just hides
the iframes away but never detaches them from the DOM.

Change-Id: I4aeb079a8757de140f7ca0affe5ece931dadfc7a
2025-03-04 08:07:58 -08:00
Camillo Bruni
a4d98e20f9 Push the latest upstream main changes to main-custom
- Perf-Dashboard.Render sync phase sets location.hash, but the actual DOM (#488)
- Add icons to developer mode selection buttons (#493)
- Update github workloads (#478)
- Score math: Add legend for variables (#490)
- Use WakeLock (#491)
- Release/3.1 - merge 428 (add id to input) (#484)

Change-Id: I9f83afd0b62bc4e2dcc74e2363d5ab732d1f918a
2025-03-04 07:41:30 -08:00
Thorsten Kober
68d8f57ddf
News Site Next: simplify message popup integration (#471) 2025-01-29 10:18:04 -05:00
Thorsten Kober
a3b0e71d97
News site Next: run build command (#474) 2025-01-23 12:19:03 -05:00
Julien Wajsberg
7d7504f9bd
Add a netlify configuration file (#472) 2025-01-22 14:40:54 +01:00
Camillo Bruni
0c3f0fb132
Add end2end tests (#470)
- Move unittests to run-unittests.mjs
- Move common test driver and server setup to separate helper.mjs module
- Add "SpeedometerReady" and "SpeedometerDone" events for easier testing
- Add simple end2end tests that invoke the benchmark via the html page
2025-01-22 12:56:07 +01:00
Thorsten Kober
9f85f8e3bd
News Site Nuxt - Article Ids (#448) 2025-01-21 11:54:13 -05:00
Thorsten Kober
2ff56c9d76
News Site Next - Article Ids (#447) 2025-01-21 11:51:56 -05:00
Thorsten Kober
d979723ef6
Async Runner with Message Channel (#458) 2025-01-15 14:30:42 -05:00
Camillo Bruni
d6b5ffea95
Display non-standard params on summary page (#469)
Make sure we better highlight non default params since they affect the default score.

- Display non-default params table in the summary view
- Simplify defaultParams creation code a bit
- Rebuild news-next since params.mjs changed
2025-01-09 17:08:47 +01:00
Camillo Bruni
4dfa8faeea
Add Measurement and Scoring details to README.md (#467)
Document in more detail how scoring and workload measurement works:
- Add example timeline to explain which parts contribute to the score
- Add basic formulas on how the test steps are combined into a final score
2025-01-08 11:26:46 +01:00
Camillo Bruni
faf9fff140
Avoid measurement dead-spot between sync and async phase (#464)
We could potentially miss a gc between the performance.mark calls for syncEndTime and asyncStartTime.

This PR changes the code to reuse the sync end times and marker for the async start.
2025-01-06 16:43:46 +01:00
Camillo Bruni
a169b1d56a
Auto run unittests in tests/index.html (#468)
Instead of triggering the tests from the outside, we can always directly start the mocha runner in tests/index.html.
The selenium driver now syncs on either globalThis.testResults being there or the test-complete event.

This way we should be able to reliably run the tests both via selenium and the browser.
2025-01-06 15:23:54 +01:00
Ryosuke Niwa
d87b307974
Delete the timer based measurement since we've switched over to (#466)
requestAnimationFrame based measurement for 3.0 and found no issues.
2024-12-20 10:14:58 -08:00
Thorsten Kober
f7c3a7014a
Remote Runner / PostMessge api (#456) 2024-12-17 17:27:08 -05:00
Ryosuke Niwa
0dc0b1c50e
Make the progress bar track indivisual suites, not indivisual steps. (#465)
This is needed to allow for remote workloads.
2024-12-17 13:37:04 -08:00
Thorsten Kober
e2e3c496f5
Fix/selenium (#461) 2024-12-09 08:05:08 -05:00
Thorsten Kober
67bc21f1f4
Runner Cleanup: SuiteRunner & TestRunner classes (#452) 2024-11-15 13:42:38 -05:00
Camillo Bruni
e70778f1a8
Add more flexible SUITE_RUNNER_LOOKUP (#438)
Add SUITE_RUNNER_LOOKUP to dynamically look up the suite runner class
2024-11-13 13:30:47 +01:00
Thorsten Kober
a64fa26876
split test-invoker (#451) 2024-11-12 20:08:11 -05:00
Thorsten Kober
e2afcb6e4e
News Site Nuxt - Images (#450) 2024-11-11 20:26:51 -05:00
Thorsten Kober
66b5324d02
News Site Next - Images (#449) 2024-11-11 20:26:29 -05:00
Thorsten Kober
94f0d81686
Test Runner cleanup - for our internal tests (#446) 2024-11-06 08:24:54 -05:00
Camillo Bruni
4f87c768c4
Add Params parse helper methods (#444)
- Move parsing to separate helper methods to main the _copyFromSearchParams method more readable.
- Add Params.prototype._parseXXX helper methods
2024-11-04 19:55:53 +01:00
Camillo Bruni
1552769e5c
Add local results variable to SuiteRunner (#445)
We can migrate some of the measuredValue logic directly to the SuiteRunner and simplify the code a bit.
Additionally this will allow us to generate more flexible metrics in the future.
2024-11-04 16:25:04 +01:00
Camillo Bruni
04cfe65d7d
Refactor developer-mode UI code (#443)
- Move default onchange handler to createTimeRangeUI
- Add updateCallback to createCheckboxUI
2024-11-04 15:20:25 +01:00
Camillo Bruni
617c7310fa
SuiteRunner: use suite instance variables (#437)
* use suite directly
* fix tests
2024-10-29 14:27:39 +01:00
Camillo Bruni
4719e3f8bd
Use TEST_INVOKER_LOOKUP (#439)
Add generic lookup dict for the test-invoker class.
This makes future extensions and local hacking easier.
2024-10-23 09:58:30 +02:00
Thorsten Kober
3ce6105a3a
Cleanup to prepare for postMessage / remote runner (#435) 2024-10-21 11:54:42 -04:00
Camillo Bruni
2387b9fe89
Split off SuiteRunner class (#433)
Move Suite-related code to a separate SuiteRunner class.
This happens in preparation for async and remote suites where it will be cleaner to handle the code paths independently instead of adding more functionality to the BenchmarkRunner class.
2024-10-21 17:16:06 +02:00
Camillo Bruni
bb4a4ad1c1
Improve developerMode styling (#436)
- Maintain the same margin to the "Developer Mode" text when closed and open
- Remove display flex that had no effect
- Make sure the run settings are always visible
- Scroll only the suites part
- Move the select all / unselect all buttons to the top for better usability
2024-10-17 16:06:24 +02:00
Camillo Bruni
2a4d041656
Remove interactive runner (#432)
- To simplify future runner refactorings we remove the interactive runner.
- Stepping functionality is added to the developer-menu and integrated directly into the default MainBenchmarkClient.
2024-10-11 08:26:53 -04:00
Camillo Bruni
c2e8d4f6bc
Improve developerMode updateURL (#431)
- Simplify the tag/suite/suites selection code by always deleting all searchParams first
- Don't use tags if there is only one selected suite (previously selecting "React-Stockcharts-SVG" currently sets the "svg" tag which is a bit unexpected)
- Compare against defaultParams for "measurementMethod" using the existing method for bool values
2024-10-10 13:30:57 -04:00
Thorsten Kober
400a5b87a0
TodoMVC - React - add id to input (#428) 2024-10-02 07:20:55 -04:00
Camillo Bruni
0dcab0317e
Use more helper runner helper methods (#421)
Refactor the runner and add more helper methods to be more flexible for upcoming experimental PRs.
2024-09-30 21:25:56 +02:00
Matthieu Dubet
ea14d27e03
Change default port to 8900 (#429)
Port 7000 is occupied by default on macOS.
This also makes it simpler how to customize it
(use env var instead of hardcoded in the package.json)

Fix https://github.com/WebKit/Speedometer/issues/325
2024-09-10 13:09:29 -07:00
Thorsten Kober
3150f41173
Add emojis to a todomvc app (#417) 2024-08-07 15:05:18 -04:00
Thorsten Kober
5a19851e54
Add local storage to todoMVC-es5 (#414) 2024-08-07 14:52:34 -04:00
Camillo Bruni
a999ad1024
Don't display invalid results (#406)
This partially fixes #399 by not display non-finite scores which are caused by 0-measured suite results.

- For non-positive or non-finite scores, the following summary page is displayed. Note that the detailed page is still accessible since you might find some useful information there.
- Additionally, a console.error is generated for each zero-sum suite
2024-07-25 09:03:30 +02:00
Konrad Kügler
e6e8a4f969
Remove obsolete eslint ignore (#410)
The offending `new Promise(async ...)` code was already removed in 19184dc694 but this linting comment remained.
2024-07-24 09:56:03 -07:00
Camillo Bruni
33dec4f328
Deconfuse Geomean Metric and geomean aggregate value (#408)
The metric's "geomean" aggregate value can be easily confused with the top-level Geomean metric (issue #407).

- Don't serialize "geomean" in the JSON data
- Add descriptions to Score, Geomean, and Iteration metrics that are serialized in the JSON data
- Make the detail view titles more descriptive
2024-05-30 17:37:45 +02:00
Camillo Bruni
3ae822bcff
Manually install Firefox on macos-latest image (#411)
macos-latest does not include Firefox yet.
Manually running brew install --cask firefox on the firefox test job solves the problem.
2024-05-29 14:47:40 +02:00
Thorsten Kober
13cc928140
[fix] News Site Nuxt: rename 'className' -> 'class' (#400) 2024-03-22 12:21:29 -04:00
Zev Eisenberg
cbf8311a76
Remove duplicated word. (#402) 2024-03-15 11:23:18 -07:00
Morgan Roderick
bd9ab77462
chore: upgrade sinon to latest (#398)
See https://github.com/sinonjs/sinon/blob/main/CHANGES.md
2024-03-12 10:55:19 +01:00
Issack John
8d67f28d02
Fix Cross-platform issue with npm script (#397)
* Make commands work cross-env

* make npm format work as well.
2024-03-07 01:41:02 -08:00
Markus Stange
3d2a28e2dc
Avoid :has selector in CSS. (#395)
This changes how section visibility is controlled.
All sections are still `display:none` by default.
Rather than becoming `display:block` when they match `:target`,
they now become `display:block` if their ID matches the value
of the root element's `data-visible-section` attribute - we hardcode
the four section names "home", "running", "summary" and "details".

This lets us default to the home section just by setting
`<html data-visible-section="home">` in index.html, and we
can avoid the `:has` selector.

Now Speedometer can run in older browsers which don't support `:has`.
2024-03-06 11:02:43 -08:00
lpardosixtosMs
406042a18d
Update license dates, add Microsoft (#393) 2024-03-05 12:09:16 -08:00
Julien Wajsberg
7efb0ac57b
Run npm audit fix on charts and editors (#390)
* Run npm audit fix on charts

* Run npm run build on charts

* Run npm audit fix on editors

* Run npm run build on editors
2024-03-05 15:30:34 +01:00
Julien Wajsberg
309437377e
Enable source maps in charts and editors (#391)
* Enable sourcemaps in editors

* Enable sourcemaps in charts
2024-03-04 12:34:43 -08:00
lpardosixtosMs
88c5323a49
Update Testing.md to include Microsoft Edge (#392) 2024-03-04 10:40:16 -08:00
Thorsten Kober
58e2e4e92b
Revert "Todomvc-vue: source-map creation (#386)" (#388)
This reverts commit 7dc43d27a5.
2024-03-01 20:19:10 -05:00
Thorsten Kober
7dc43d27a5
Todomvc-vue: source-map creation (#386) 2024-03-01 19:19:56 -05:00
Thorsten Kober
ad061acd81
run format command from root (#387)
Co-authored-by: Thorsten Kober <thorstenk@google.com>
2024-03-01 16:53:11 -05:00
Ryosuke Niwa
5654961621
Update about.html to link to each test's GitHub directories instead of directly linking to README.md files. (#384) 2024-02-29 17:56:23 -08:00
Issack John
377c6c7610
Fix Hidden dropdown menus (#376)
closes #362
2024-02-29 09:38:20 -08:00
Brian Grinstead
3ca4aed3a8
Remove experimental note from README.md (#361) 2024-02-29 09:33:37 -08:00
Camillo Bruni
c901052f2d
Add warmup and step delay to developer UI (#323)
Drive-by-fix:
- Move the more frequently used iteration count to the top
- Move warmupSuite and warmup-delay closer together
2024-02-29 10:46:39 +01:00
lpardosixtosMs
899c11317d
Enable local testing in Edge (#382) 2024-02-28 10:41:23 -08:00
Issack John
82e0a46283
Make about.html text reflect currently enabled workloads (#381)
* make text reflect currently enabled workloads
2024-02-27 09:48:24 -08:00
Camillo Bruni
f6f67e0237
Update General Instructions (#377)
Update general instructions on how to run Speedometer 3 with generic measurement advice.
2024-02-27 10:50:10 +01:00
Thorsten Kober
0c3af704a5
Rebuild workloads (#375) 2024-02-20 11:13:07 -05:00
Julien Wajsberg
ca9d23c18f
Make the github action linter step fail in the case of issues (#374)
* Make the github action linter step fail in the case of issues

* Run the formatter on the project
2024-02-17 12:26:43 +01:00
Ryosuke Niwa
3d782244a5
Add the source map for Angular TodoMVC build. (#332) 2024-02-15 11:54:42 -05:00
Brian Grinstead
2640affe64
Update about.html (#372)
Fixes #344
2024-02-09 15:47:05 -08:00
Julien Wajsberg
0fd1528419
jQuery workloads: use :nth-child() in a querySelector call in each iteration (#367)
This PR fixes the issue #365: jQuery test only ever marks the first todo item as completed

The bug was caused by jQuery replacing the whole todo list whenever each todo item is toggled.

Fixed it by running querySelector with :nth-child in each iteration.

Co-authored-by: Ryosuke Niwa <rniwa@webkit.org>
2024-02-08 10:59:05 +01:00
Camillo Bruni
9837ccc768
Add separate about.html and instructions.html (#359)
Move the about section and the newly created instruction section into a standalone
html file to simplify the in-page navigation for the runner.
2024-02-02 10:53:06 +01:00
Thorsten Kober
7b1b81ee13
fix clear-completed (#364) 2024-01-31 12:57:07 -05:00
Issack John
285e32dacb
Npm audit fix (#360)
* run npm audit fix in affected packages
* rebuild all touched dists
2024-01-30 14:28:55 -08:00
Camillo Bruni
31ba222f5b
Remove test-again button (#358) 2024-01-25 15:00:46 +01:00
Camillo Bruni
7529eedcc5
Adjust dot size to iterationCount (#356)
Gradually adjust scatter plot dot sizes from 5px down to 2px for iterationCount > 20.
2024-01-25 13:38:02 +01:00
Camillo Bruni
638a9681cf
Update document title (#354)
Use a "data-title" attribute for each section to update document.title.
2024-01-25 11:21:55 +01:00
Camillo Bruni
b0a465d60c
Fix navigation recursion (#357)
Conditionally update window.location.hash for #home to avoid navigation recursion.
2024-01-25 11:17:48 +01:00
Camillo Bruni
3d93f179b6
Fix navigation for Firefox (#353)
Refreshing the page with results and a #summary hash breaks on Firefox.
Explicitly setting the #home has a workaround cleanly shows the main page again.
2024-01-25 10:21:12 +01:00
Julien Wajsberg
7c0931ee8c Run format on a previously unformatted file 2024-01-23 14:07:58 +01:00
Julien Wajsberg
7ae836e55f Configure the next eslint plugin so that there's no false positive error when running eslint 2024-01-23 14:07:58 +01:00
Julien Wajsberg
c0269db7d7 Add root: true to the top level eslint configuration file 2024-01-23 14:07:58 +01:00
Julien Wajsberg
6ad43c3f5c Makes tsx files checked by the formatters. 2024-01-23 14:07:58 +01:00
Julien Wajsberg
0f449aa702 Remove tentative from the prettier and eslint ignore lists 2024-01-23 14:07:58 +01:00
Camillo Bruni
4b451d8388
Allow empty hash for cleaner default URLs on the home screen (#349)
This makes sure we don't need the #home hash for the default page URL.
2024-01-17 10:33:43 +01:00
Issack John
64a5e7c0ba
Fix Complex vs. non-complex DOM subtests order (#343)
* interleave the stand-alone and the complex dom tests
2024-01-11 21:28:33 -08:00
Ryosuke Niwa
2aa819faed
Always instantiate Float32Array in the cost array of findOptimalSegmentationInternal. (#342) 2024-01-11 15:50:12 -08:00
Issack John
91aaaacb42
Regenerate package-lock.json (#339)
* update package-locks
2023-12-21 11:06:20 -08:00
Issack John
4173e4d8a9
Enable complex DOM benchmarks execution in official speedometer run (#271) 2023-12-21 10:28:08 -08:00
Issack John
a268ec4391
Fixes #335: Prevent complex DOM workloads from hitting a specific WebKit pathology (#338)
* Utility function to create css variants for big-dom.css
* update build scripts
* update dists
2023-12-21 10:12:49 -08:00
Julien Wajsberg
2414364b18
Keep the parameter 'tags' as an array in the body of the function (#337)
Previously, we were transforming it into a set. This was giving problems
in a later condition, so that no message/debugInfo were defined, and the
user would get an alert with the message "undefined".

Now the set stays local to the code block where it's used, so that the
parameter variable "tags" is an array for the whole function, for more
consistency with the "names" variable.
2023-11-29 19:17:06 +01:00
Camillo Bruni
208e1abee2
Avoid setTimeout in waitOnElement (#321)
waitOnElement uses a 50ms timeout, which was historically motivated by FlightJS that used requirejs which had a hard-coded 50ms pause.

We've observed fragile CPU scaling behavior with this approach in Speedometer 2.1. V3.0 is less affected by this since the perf-dashboard is the only workload the regularly needs another cycle to wait (all other workloads are ready after onload has fired).

Using rAF lowers the timeout limit and is more realistic in required CPU ramp-up performance.
2023-11-20 10:56:59 +01:00
Camillo Bruni
2ee210ca20
Add "Start Test" developerMode button (#333) 2023-11-14 20:48:00 +01:00
lpardosixtosMs
abea7b89b9
[Complex-Dom] Combine visibility: hidden and display: none for side TreeView hidden elements. (#327) 2023-11-01 07:50:03 -07:00
Thorsten Kober
c19468aa56
TodoMVC web component: change css rule, run build on complex (#326) 2023-10-18 18:50:45 -04:00
Thorsten Kober
32c7c23a7d
Todomvc svelte cleanup (#324) 2023-10-10 10:48:08 -04:00
Camillo Bruni
b77b3dbf2b
Fix developerMode layout (#322)
- Fix scroll content overflow
- Remove li spacing in the developer menu
2023-10-05 12:42:40 +02:00
Thorsten Kober
44c809305d
todomvc web-components, change title, id & completed attributes on item component (#314) 2023-10-02 16:17:23 -04:00
Issack John
4316b5fb14
Follow up for complex DOM versions (#315)
* Check prereqs in the build script and handle them
* add code comments
* Update build scripts & package.jsons
2023-09-29 11:45:58 -07:00
Thorsten Kober
81c835f6b7
TodoMVC Angular Bug Fixes (#312) 2023-09-25 12:10:37 -04:00
lpardosixtosMs
8fd080fb67
[Complex-DOM] Fix generator node count (#308)
* Fix count
* Rewrite generator
* Wrap the generator in a function
* combine folderwrapper and treeitem
* add class for type of item
* Update dist folders

---------

Co-authored-by: Julien Wajsberg <felash@gmail.com>
Co-authored-by: Issack John <issack.john@outlook.com>
2023-09-21 12:36:40 -07:00
Camillo Bruni
b10452595e
Set proper unit for "Score" Metric (#311) 2023-09-12 22:33:43 +02:00
lpardosixtosMs
0672c38f23
Add complex DOM workloads to tests.mjs disabled by default (#310) 2023-09-08 09:43:58 -07:00
Thorsten Kober
790eed91c9
reverse lit/web components deletion steps (#309) 2023-09-07 09:09:49 -04:00
lpardosixtosMs
679cb52fdf
Add Web Components and LIT versions of Complex-DOM TodoMVC (#285)
This PR does not enable any test in the runner.

Co-authored-by: issack john <issackjohn@microsoft.com>
2023-09-06 09:23:41 -07:00
lpardosixtosMs
5748e44342
Delete extra line (#307) 2023-09-01 19:30:28 +02:00
lpardosixtosMs
c306d4237d
run npm format (#306) 2023-09-01 18:59:35 +02:00
lpardosixtosMs
095abdfc44
Delete duplicated React Complex DOM README (#305) 2023-08-30 10:44:25 -07:00
lpardosixtosMs
a03f1367ea
Add Complex DOM READMEs (#301)
Add READMEs for the big-dom-generator and each complex DOM variation.

---------

Co-authored-by: Issack John <issack.john@outlook.com>
Co-authored-by: Sulekha Kulkarni <Sulekha.Kulkarni@microsoft.com>
2023-08-29 19:02:21 -07:00
lpardosixtosMs
f6c5098d6e
Remove "targeted", "li-<index>", and "view-<index>" class names from todoMVC (#300)
The "targeted", "li-", and "view-" class names were added for the complex DOM generated CSS, the handcrafted CSS that replaces it does not use them anymore.
2023-08-23 10:09:38 -07:00
Thorsten Kober
f841f81faa
Speedometer About format & readme links (#294) 2023-08-22 10:08:29 -07:00
Camillo Bruni
a608e98396
Remove broken benchmark-runner.mjs file (#297)
The file is currently unused and was webkit-CI specific.
2023-08-21 12:12:59 +02:00
Camillo Bruni
fb9b966b37
Add metrics getter (#298)
This should simplify extracting metrics from the default page.
2023-08-21 12:08:13 +02:00
lpardosixtosMs
b3ec8c748b
Fix :last-child selector in Web Components and LIT (#296) 2023-08-18 11:12:25 -07:00
Doug Thayer
d86dad330f
Add param for shuffling suite order between runs (#292)
* Add param for shuffling suite order between runs

* pr feedback

* move into runner-prepare and log seed
2023-08-18 08:32:22 -07:00
lpardosixtosMs
5107c739c1
Replace generated complex CSS with handcrafted CSS (#291)
* Replaced generated css with complex handcrafted CSS.



---------

Co-authored-by: Luis Fernando Pardo Sixtos <lpardosixtos@microsoft.com>
Co-authored-by: lpardosixtosMs <94007365+lpardosixtosMs@users.noreply.github.com>

* Move reused logic into buildComplex Util function
* Update build scripts

---------

Co-authored-by: Issack John <issack.john@outlook.com>
2023-08-15 09:59:45 -07:00
Brian Grinstead
b6abd32982
Update about text (#293) 2023-08-09 12:09:56 -07:00
Ryosuke Niwa
0fef685dcb
Simplify the score calculation by replacing 60 / 50 by 1. (#289) 2023-07-26 11:58:01 -07:00
Ryosuke Niwa
26531fc7f1
Fix the developer menu to set measurement method only when it's not RAF-based. (#290) 2023-07-24 13:08:22 -07:00
Issack John
64798bc754
[big-dom-generator] Remove non-inclusive language (#288)
* Remove non inclusive language
* Rebuild vue-complex
2023-07-19 13:45:18 -07:00
lpardosixtosMs
5491573d0e
Rebuild react-redux-complex on top of sanitized standalone (#286) 2023-07-17 16:47:45 -07:00
Thorsten Kober
8f6982e71a
Add Sanitizing script to react-redux and vue to remove non-inclusive words in dist folders (#284) 2023-07-13 10:04:17 -07:00
Issack John
cba2b0d2e7
Complex DOM versions of todoMVCs (#264)
This PR introduces complex DOM versions of the todoMVCs.
---------

Co-authored-by: lpardosixtosMs <94007365+lpardosixtosMs@users.noreply.github.com>
Co-authored-by: Luis Fernando Pardo Sixtos <lpardosixtos@microsoft.com>

* use path.resolve to compute paths from __dirname
* Refactor package names + PR comments (#31)
* update build scripts
* more verbose build done message
---------

Co-authored-by: lpardosixtosMs <94007365+lpardosixtosMs@users.noreply.github.com>
Co-authored-by: Luis Fernando Pardo Sixtos <lpardosixtos@microsoft.com>
2023-07-13 09:50:56 -07:00
Thorsten Kober
36712deb57
Remove es6 todomvc (#283)
Since the es6-webpack workload doesn't transpile down to any earlier versions of EcmaScript, testing two todomvc workloads with similar logic is redundant.
2023-07-13 16:58:09 +02:00
Thorsten Kober
f02d7e4b64
News Site CSS Cleanup (#275) 2023-07-12 13:57:50 -07:00
Thorsten Kober
61ad820a5e
use translations (#279) 2023-07-12 09:58:28 -07:00
Ryosuke Niwa
d834dcbb07
Fix a typo in Metric class' regular expression. (#282) 2023-07-12 09:14:54 -07:00
Camillo Bruni
a9c96c3bd4
add npm run server to run caching server (#281)
`npm run server` will now run a local server with caching files.
This is more realistic for local benchmarking, while `npm run dev` keeps on the non-caching behavior which is nicer for developing the benchmark.
2023-07-11 23:36:34 +02:00
Ryosuke Niwa
e11801e3b5
Fix the developer menu's measurement method toggle. (#278) 2023-07-11 13:19:11 -07:00
Ryosuke Niwa
e2a033d54b
Remove superfluous conosle.log in the perf dashboard workload. (#277) 2023-07-11 13:18:42 -07:00
Thorsten Kober
91f54a9c6d
add translations for the text used in the tests of the todmvc apps (#259) 2023-07-11 06:20:04 -07:00
Issack John
95af4c4956
bug fix (#276) 2023-07-10 13:35:42 -07:00
Thorsten Kober
e8624b1096
News site - add translation data (#272) 2023-07-07 14:30:21 -07:00
Peter Burns
f6793b0bd5
Generate lit/dist/index.html (#270)
Fixes https://github.com/WebKit/Speedometer/issues/269
2023-07-05 12:13:11 -07:00
Issack John
815817b5e0
Remove extra semicolon from react-redux todomvc (#268) 2023-07-03 12:05:45 -07:00
Ryosuke Niwa
b44d98255f
Change the default measurement method to rAF-based approach (#262) 2023-06-30 12:23:06 -07:00
Thorsten Kober
151d73d535
[OPTIONAL] --NUXT--Newssite - updates to support different languages / html direction in the future (#265) 2023-06-30 09:13:57 -07:00
Thorsten Kober
8046654389
TodoMVC Web Component - use local todomvc-css package (#263) 2023-06-29 14:14:21 -07:00
Thorsten Kober
9025eb1fdc
[OPTIONAL] - Newssite - updates to support different languages / html direction in the future (#248) 2023-06-29 07:11:39 -07:00
Issack John
ad36a2c483
Refactor Complex DOM CSS generator (#246)
Refactored the CSS generator to use JSDOM and html markups.
2023-06-28 19:00:31 -07:00
Peter Burns
9e692e4074
Add a Lit TodoMVC app (#251) 2023-06-28 14:24:18 -07:00
Thorsten Kober
68c56ff662
[OPTIONAL] - updated todomvc styles (#254) 2023-06-28 14:03:11 -07:00
Ryosuke Niwa
1c4d9c7d10
Move Perf Dashboard suite out of tentative directory. (#244) 2023-06-28 12:52:47 -07:00
Ryosuke Niwa
0f6443e065
Avoid using generators in TimeSeries data model. (#256)
This avoids pathological perf hit in Firefox.
2023-06-28 11:53:10 -07:00
Thorsten Kober
8491149024
use external text (#260) 2023-06-28 11:19:16 -07:00
Julien Wajsberg
1f48e366d0
Run the format script after javascript web components landed (#261) 2023-06-28 09:47:44 -07:00
Julien Wajsberg
bd4dc60b24
Move stockcharts out of tentative (#231)
* Move stockcharts out of tentative

* Add some explanations about how to change react-stockcharts locally
2023-06-28 17:15:51 +02:00
Julien Wajsberg
27c0b2cddf
Move the charts workload out of the tentative directory (#252) 2023-06-28 17:15:28 +02:00
Thorsten Kober
2281e13426
[OPTIONAL]: TodoMVC built with Web Components (#249) 2023-06-28 07:30:08 -07:00
Thorsten Kober
18c6ed85d9
Use more varied set of todo text rather than Something to do ${i} (#258) 2023-06-27 15:28:25 -07:00
Ryosuke Niwa
00cac75b70
Move the initial glyph cache cost out of the measured part of the perf dashboard workload. (#255) 2023-06-23 14:16:24 -07:00
lpardosixtosMs
039f2e854f
Add Complex DOM required changes to standalone TodoMVC apps (#239) 2023-06-23 09:41:03 -07:00
Julien Wajsberg
b4f675ef5f
[charts] Group substeps to make them more stable (#220) 2023-06-23 14:52:35 +02:00
Thorsten Kober
03f74f8b4c
Adjusting Page.querySelector / Page.querySelectorAll (#253) 2023-06-22 14:49:39 -07:00
Julien Wajsberg
73fda50cb2
Group the Create step into the next step (#223) 2023-06-20 21:50:30 +02:00
Julien Wajsberg
e3b8e93ec4
Rebuild the editors code after #241 landed (#245) 2023-06-20 11:19:06 +02:00
Thorsten Kober
c7eef15502
[Optional] : News site Nuxt - use hash router (#238) 2023-06-19 20:52:14 -07:00
Camillo Bruni
42a7b3b5d6
Use progress element (#233) 2023-06-19 17:23:13 +02:00
Ryosuke Niwa
d50264b547
Undefine requestIdleCallback in the Next test for consistency. (#243)
We can undo this change once Safari / WebKit adds the support for requestIdleCallback.
2023-06-18 14:14:28 -07:00
Ryosuke Niwa
4e13390704
Undefine requestIdleCallback in the code mirror test for consistency. (#241)
We can undo this change once Safari / WebKit adds the support for requestIdleCallback.
2023-06-18 14:14:10 -07:00
Ryosuke Niwa
0b89992a1a
Undefine requestIdleCallback in the Nuxt test for consistency. (#242)
We can undo this change once Safari / WebKit adds the support for requestIdleCallback.
2023-06-17 12:17:36 -07:00
Julien Wajsberg
054b6342c6
Move the editor workload out of tentative (#214)
Co-authored-by: Brian Grinstead <briangrinstead@gmail.com>
2023-06-14 11:22:32 -07:00
Thorsten Kober
7efcc89499
move newssites out of tentative (#229) 2023-06-14 11:10:10 -07:00
Thorsten Kober
3b012c27f0
Newssite nuxt format (#235) 2023-06-13 15:01:08 -07:00
Thorsten Kober
387720d1a7
Newssite next.js format (#236) 2023-06-13 14:49:52 -07:00
Camillo Bruni
0da3406f1b
Developer Mode: Support filtering by tags (#217)
- Add Tags filtering buttons
- Update url with tag if a single tag can be used to represent the current selection
- Make sure auto-added tags are first in Tags and suite.tags
2023-06-13 22:19:15 +02:00
Camillo Bruni
a5d8934b12
Reduce version number letter spacing (#234)
- Lower version number spacing a bit
- Drive-by-fix: Add Roboto as slightly better alternative font.
2023-06-13 01:17:04 +02:00
Thorsten Kober
392d54feaa
Newssite css: cleanup (#232) 2023-06-12 10:01:46 -07:00
Camillo Bruni
e295eb8596
Display iteration marker in progress bar (#228) 2023-06-09 13:41:10 +02:00
Thorsten Kober
b8b72cda46
run format (#230) 2023-06-08 16:28:35 -07:00
Ryosuke Niwa
38d5a701c0
Add the missing tags to perf dashboard workload. (#222) 2023-06-08 19:25:52 -04:00
Camillo Bruni
df082e68c5
Auto-add empty tags (#226) 2023-06-08 23:30:05 +02:00
Camillo Bruni
1cac2f1166
Show version number in all sections #227 2023-06-08 23:29:45 +02:00
Daniel Roe
951f57b106
Set base url dynamically for Nuxt site (#225) 2023-06-08 13:52:48 -07:00
Ryosuke Niwa
d19454c8e8
Add WebKit's performance dashboard as a workload. (#156)
It's a website written in ES2015+ with web components.
The workload emulates openinig the charts page, selecting data points,
then selecting a data range.
2023-06-08 14:30:33 -04:00
Thorsten Kober
7465b1a9a8
add tags to nuxt (#218) 2023-06-08 07:25:57 -07:00
Thorsten Kober
28fbc651da
News Site - Nuxt version (#203) 2023-06-08 07:11:25 -07:00
Camillo Bruni
32a381fcab
Add Suite tags for complex filtering (#215)
Mostly self-explanatory:
- Each suite has a set of tags
  - "all" applies to all suites
  - "default" applies to suites that are enabled by default (and not tentative)
  - "tentative" applies to suites that are in the tentative directory
- `&tags=` can be used for filtering Suites by tag
2023-06-08 10:42:42 +02:00
Camillo Bruni
a0c756901d
Developer Mode UI Improvements (#216)
- Enlarge clickable targets (title and list items)
- Make buttons fill space horizontally
- Match border radius with other runner buttons
2023-06-08 10:22:32 +02:00
Thorsten Kober
c0d0626159
News site next updates (#206) 2023-06-07 18:37:09 -07:00
Julien Wajsberg
8d13cdf385
Reduce the size of the long text loaded in TipTap to reduce its running time (#208) 2023-06-07 11:40:08 +02:00
Thorsten Kober
0d8bf1226d
use nanoid for preact / react todomvc (#211) 2023-06-06 16:38:33 -07:00
Julien Wajsberg
282e2b1df7
Reduce the react stockcharts steps (PR #200) 2023-06-06 18:03:09 +02:00
Camillo Bruni
0940a7700c
Show version number (#213) 2023-06-06 16:41:46 +02:00
Camillo Bruni
c46e191e52
Fix params parsing (#212) 2023-06-05 18:02:25 -07:00
Camillo Bruni
3a678028a5
Add param.waitBeforeSync (#202)
- Add waitBeforeSync to set a timeout before running the sync step
- Change warmupBeforeSync to use 0 as default value
- Add more params comments
- Add common TestInvoker base class
2023-06-06 00:32:05 +02:00
Camillo Bruni
ec1326cf1a
Freeze params again in non-developerMode (#209)
This prevents accidental modification in normal runs.

Drive-by-fix:
- Fix comment and run `npm run format`
2023-06-06 00:26:12 +02:00
Camillo Bruni
91d378683e
Add stricter _parseInt helper to Params (#210)
- `parseInt` is too permissive and easily parses partial values like `100warmupBeforeSync=10`.
  Using `Number` and `Number.isInteger` instead to give stricter warnings.
- Add more descriptive error messages
- Use fewer default fallbacks when explicitly passing along params
2023-06-06 00:16:42 +02:00
Julien Wajsberg
39e9a52436
Replace innerHTML with DOMParser in the javascript-es5 implementation (#159) 2023-06-05 18:07:45 +02:00
Julien Wajsberg
b8a24fdbb0
[codemirror] Mock requestAnimationFrame to setTimeout so that we don't lose any work in the async time (#205) 2023-06-01 21:14:52 -07:00
Julien Wajsberg
e1957e3919
Set the font-size and text-size-adjust to alleviate differences between browsers (#198) 2023-05-31 10:05:48 -07:00
Ryosuke Niwa
ec51e3188b
Add warmupBeforeSync parameter, which triggers speciefied milliseconds of infinite loop before each sync step. (#196)
This will allow us to evaluate the impact of using rAF vs. timer with regards to CPU throttling.
2023-05-31 11:53:53 -04:00
Julien Wajsberg
4f9ffb5e3b
Stockcharts: Mock requestAnimationFrame to setTimeout so that the runner can catch asynchronous work (#176)
Fixes #97
2023-05-31 08:52:19 -07:00
Julien Wajsberg
e2530dbed3
Add a workload with Chart.js (#149) 2023-05-31 08:51:36 -07:00
Camillo Bruni
7dc36290eb
npm run format (#191)
* run formatting again
* make linter happy
* adding newlines
2023-05-30 22:32:17 +02:00
Camillo Bruni
07a4618a83
Better copy-support for detailed results table (#192)
Make sure we can easily copy the whole detailed results page as formatted text.

- Only toggle charts when clicking on charts
- Don't copy chart legend bullet
- Don't copy chart axis labels

Drive-by-fix:
- Put abbreviation ellipsis for chart legends at the beginning so we don't hide the more important metric suffix
2023-05-30 09:04:08 +02:00
Camillo Bruni
43a7448ca3
Add more markers (#190)
Adding more markers to simplify checking if work is measured:

- Add iteration-X for each runner iteration
- Add runner-prepare and runner-finalize to annotate time it takes for adding/removing the iframe
- Use suite-X-prepare
2023-05-30 09:02:49 +02:00
Brian Grinstead
232cadb2ab
Developer Mode UI improvements (#194)
* update range label on input, not change

* enable ctrl/cmd+click on both the input and the label for suite selection in devmode
2023-05-25 21:00:38 -07:00
Ryosuke Niwa
1bc9ece1f5
Adjust the multiplier to convert geomean so that scores are less than 50 in even the fastest browser/device. (#189) 2023-05-25 16:27:15 -07:00
Brian Grinstead
170710fdad
Additional developerMode controls (#188) 2023-05-25 09:22:05 -07:00
Julien Wajsberg
e0c9ed813e
[editor] Use longer text in the benchmarks (#186) 2023-05-24 16:31:13 -07:00
Thorsten Kober
dca334b044
test path (#187) 2023-05-24 12:19:52 -07:00
Ryosuke Niwa
a876f0e419
Add requestAnimationFrame-based measurement method. (#173)
This PR adds a new query string parameter requestAnimationFrame, which takes
the value of either "timer" or "raf". "timer" is the existing and default
measurement method. When it's "raf", we use two consecutive requestAnimationFrame
first of which runs the sync step and second of which schedules a 0s timer to
signify the end of async step.
2023-05-24 11:50:11 -07:00
Julien Wajsberg
34c169c724
Rename "big" and "small" to "long" and "short" (#184) 2023-05-24 11:33:04 -07:00
Ryosuke Niwa
19184dc694
Refactor test callbacks into TestInvoker class. (#164)
This class abstracts away two nested setTimeout calls and requestAnimationFrame.
2023-05-24 11:16:03 -07:00
Julien Wajsberg
36a320c98e
Remove Stockcharts-Canvas steps (#181)
Fixes #97
2023-05-24 10:53:23 -07:00
Julien Wajsberg
82f7c6e2b8
Add a meta viewport tag so that the viewport ias always the same size on all mobile browsers (#183)
Fixes #130
2023-05-24 10:52:02 -07:00
Thorsten Kober
7c94f49293
use home link (#182) 2023-05-24 10:51:30 -07:00
lpardosixtosMs
acbb9b442b
Tentative Workload: Embed TodoMVC-React in a big DOM with complex CSS. (#150) 2023-05-24 10:50:04 -07:00
Thorsten Kober
dbc708200f
newssite - potential fix for test (#180) 2023-05-24 00:48:13 -07:00
Thorsten Kober
e52236bfc5
adjust test (#178) 2023-05-23 18:39:40 -07:00
Thorsten Kober
372bd7a018
News Site - Navigation workload (#167) 2023-05-23 15:03:08 -07:00
Camillo Bruni
e2e1fcd94b
Add detailed JSON downloads (#146)
For the upcoming comparison view we will need the separate data points which are not available in the existing JSON format.
- Add `Download classic JSON` button
- `Download JSON` and `Copy JSON` now provide a serialized version of the new metrics
- Make sure there are not loops in the metrics structure to JSON.stringify them directly
- Rename "Total" to  "Geomean"
2023-05-23 19:57:15 +02:00
Camillo Bruni
cfa5eccb3b
Format all code (#177) 2023-05-23 19:56:01 +02:00
Brian Grinstead
d8e35c4921
Add a basic developer mode ui on the main runner (#166)
Fixes #158
2023-05-19 17:08:40 -07:00
Thorsten Kober
5cf8184de4
[TodoMVC]: replace crypto.randomUUID (#168) 2023-05-19 16:40:57 -07:00
Thorsten Kober
774b018832
position iframe (#170) 2023-05-19 16:29:25 -07:00
Ryosuke Niwa
2193fda65d
Make confidenceIntervalDelta work with more than 100 degrees of freedom. (#171)
Fixes https://github.com/WebKit/Speedometer/issues/138
This allows the test runner to support an iteration count higher than 100.
2023-05-19 16:25:31 -07:00
Thorsten Kober
40136381fb
add missing map (#169) 2023-05-19 14:18:59 -07:00
Ryosuke Niwa
822906a474
Remove base element from angular example. (#161)
This allows the benchmark to be ran on a subdirectory.
2023-05-19 09:44:30 -07:00
Julien Wajsberg
f196015fa7
Run the build scripts for all vanilla examples (#160) 2023-05-19 09:36:14 -07:00
Julien Wajsberg
898398f07f
Make the number of airports per group configurable and use it in tests (#155)
* Make the number of airports per group configurable

* Add test steps to run drawing the graph with more items in each state
2023-05-18 11:12:23 -07:00
Camillo Bruni
41bdda72e8
Use metrics for CSV (#137)
- Enforce better default order of metrics
- Introduce Metric.separator to be used by metrics-ui and CSV generation
- Use Geomean instead of Total
2023-05-17 01:05:58 +02:00
Camillo Bruni
daa720b67a
Add optional warmup test suite (#124)
We've noticed that we get lazy compilation events for test-runner methods.
By doing running a warmup suite first, we can avoid this addition noise during measurements.

- Add Warmup suite
- Add params.useWarmupSuite option
- Add better performance.measure annotation for all suites
2023-05-16 15:25:11 +02:00
Camillo Bruni
ac59c3fb1e
Use more performance.mark for suites (#154)
- Use more performance.mark for whole suites.
- Pre-build the mark strings upfront
2023-05-15 23:42:27 +02:00
Julien Wajsberg
373580bec9
Add a new charting workload using observable plot (#144)
* Add observable plot

* Add a grouped bar chart

* Add dot chart

* Add a reset button and minor refactoring

* Regenerate

* Cache mapped state for states

* Remove one intermediary array

* Comments tweaking

* Use vite machinery to inject the csv files as string in the production build

* Simplify the test

* Comment the Grouped Chart example

* Review comments: add more comments, extract some values to constants, and various other small things

* Add a README file

* Moved the developer page to developer.html and add a new index

* Remove the graph comments better, remove the unused parameter in the call to parseAirportsInformation and extract the number of airports to keep to a constant

* Remove addGroupedBars and related code

---------

Co-authored-by: Brian Grinstead <briangrinstead@gmail.com>
2023-05-15 13:27:53 -07:00
Thorsten Kober
0c64eb67a9
change title casing for vue todomvc app (#152) 2023-05-12 15:22:16 -07:00
Thorsten Kober
e046a17583
Todomvc angular update / cleanup (#142) 2023-05-12 09:19:16 -07:00
Brian Grinstead
238ffeed42
reduce flashing by setting a light background to better match the content of the tests (#151) 2023-05-11 15:20:16 -07:00
Thorsten Kober
e1e156febb
Todomvc svelte 02 - Optional (#145) 2023-05-02 08:13:43 -07:00
Ryosuke Niwa
19ae86221b
Don't specify the base URL as an absolute path. (#148)
This allows the benchmark to be ran inside another directory in a local HTTP server.
2023-04-20 16:51:30 -07:00
Camillo Bruni
08c4983721
Add Metric.separator (#147)
Use / as new metric separator.
2023-04-19 21:50:17 +02:00
Thorsten Kober
10a3acfb62
TodoMVC preact readme update (#143) 2023-04-18 08:20:54 -07:00
Thorsten Kober
75cddea7ce
Todomvc vue 04 - use component for item (#139) 2023-04-12 11:31:00 -07:00
Thorsten Kober
248bfec3c4
Todomvc svelte 01 (#135) 2023-04-11 16:42:36 -07:00
Thorsten Kober
24b447eeb3
Todomvc angular 01 (#129) 2023-04-11 11:02:11 -07:00
Thorsten Kober
03ea073d46
Todomvc vue 03 - Cleanup (#134) 2023-04-11 10:56:21 -07:00
Thorsten Kober
05b6f7a08d
Todomvc vue Step 01 (#114) 2023-04-07 10:22:11 -07:00
Thorsten Kober
ab819fa534
Todomvc jquery update version (#125) 2023-04-07 10:20:42 -07:00
Brian Grinstead
652fe1bc86
Remove TodoMVC-Ember (#128) 2023-04-05 13:06:35 -07:00
Thorsten Kober
9a4cf52cd6
cleanup (#126) 2023-04-03 18:43:03 -07:00
Thorsten Kober
e9565fbb1f
Ember - Step 2 - format from root / cleanup (#119) 2023-03-30 18:13:18 +02:00
Camillo Bruni
5e6adbfe19
Improve graphs (#123)
- Crop negative values in standard view (no functional change over the current version)
- Show 0-line in normalized view
- Format normalized value tooltip better
- Use camelCase for more variables
2023-03-29 09:25:34 +02:00
Brian Grinstead
bb2ea5f183
Add CSV export (#122) 2023-03-28 15:40:42 -07:00
Brian Grinstead
c1ed921716
make details view fill available space (#121) 2023-03-27 09:02:39 -07:00
Thorsten Kober
1d46644e9b
rename todomvc tests (#118) 2023-03-23 16:24:18 -07:00
Thorsten Kober
612f748595
Todomvc styles - consistent versions (#115) 2023-03-23 14:11:20 -07:00
Thorsten Kober
619b96f901
Todomvc ember step 01 (#117) 2023-03-23 11:19:55 -07:00
Thorsten Kober
bd88f95079
TodoMVC - run format from root (#113) 2023-03-23 08:41:23 -07:00
Thorsten Kober
0744a3eb3a
Todomvc preact 02 (#108) 2023-03-16 15:50:42 -07:00
Thorsten Kober
d75589873f
Todomvc react update (#98) 2023-03-16 15:21:22 -07:00
Thorsten Kober
1afa78682d
Todomvc react redux update (#99) 2023-03-16 15:08:34 -07:00
Camillo Bruni
2e4baeefc4
adding favicon (#111)
Using the the "o" from the speedometer logo.
2023-03-16 20:50:14 +01:00
Camillo Bruni
90562b145f
Use "Enter" instead of "ENTER" for runner KeyboardEvent (#110) 2023-03-16 19:10:12 +01:00
Camillo Bruni
6d04874961
fixing iteration (#109)
- Don't calculate standard deviation for single values
- Center single data points on scatter plots to fix track alignment

This resolves #107
2023-03-16 16:29:24 +01:00
Brian Grinstead
6c31329c83
Emit start-suite and end-suite performance marks outside of the timed region (#106) 2023-03-15 16:52:38 -07:00
Thorsten Kober
1933672b81
Todomvc preact - Phase 1 (#104) 2023-03-15 08:27:53 -07:00
Thorsten Kober
09210bc05e
Todomvc backbone update (#101) 2023-03-14 10:29:04 -07:00
Camillo Bruni
6523f66497
Use Metrics Visualisations from GrandPrix (#42)
- Use scatter-plots for each metrics and submetrcs
- Show tables for each metric
- Use more CSS color variables
- Update scrollbar to match existing color scheme
2023-03-13 14:43:47 +01:00
Camillo Bruni
359605012f
Allow custom section sizes (#92)
This PR makes each runner section independent to allow custom sizes.
This is in preparation for PR #42 to allow more space for the details view.

- Move border layout from `main` to each `section`
- Copy logo into every `section`
- Use grid-layout for header/content/footer in about and details view
2023-03-13 11:18:32 +01:00
Brian Grinstead
f16557016d
Two new tentative editor workloads (#81) 2023-03-10 13:22:44 -08:00
Thorsten Kober
354d5039c4
adjust eslint (#95) 2023-03-10 11:15:46 -08:00
Thorsten Kober
2c78996286
Todomvc es6 webpack updates (#85) 2023-03-09 08:20:13 -08:00
Camillo Bruni
a20855e4a8
Fixing test (#93)
I've accidentally broken the tests by refactoring runner methods.
2023-03-09 17:16:09 +01:00
Thorsten Kober
52d094dd0e
Todomvc es6 updates (#84) 2023-03-09 07:22:32 -08:00
Camillo Bruni
ce1b63420f
Use window.location.hash for navigation (#91)
- Remove history state 
- Prevent accidentally restarting benchmark in `startBenchmark()`
- Add "home" link to about page
- Add explicit logic in `_showSection(...)` for selecting the allowed views
- Use a-href that point to the shown section
- Use CSS rules to show/hide sections (in preparation for making the details full-width)

Visible change: All urls now have a explicit hash (default is `#home`)
2023-03-09 09:40:19 +01:00
Camillo Bruni
ddd75e2206
Improve runner (#90)
- Pre-allocate all mark-label strings to avoid potential gc's during sensitive measurements
- Move code to new separate `_recordTestResults` method in preparation for pr #28
2023-03-09 09:21:51 +01:00
Brian Grinstead
999f4f676b
Initial dev mode param implementation (#89) 2023-03-08 20:33:42 -08:00
Thorsten Kober
33b5e9ae76
Todomvc es5 updates (#82) 2023-03-07 14:58:05 -08:00
Thorsten Kober
6f683630ef
use consistent names (#87) 2023-03-07 13:57:27 -08:00
Camillo Bruni
a068b53a7e
Fix score view (#86)
- Use params directly in MainBenchmarkClient
- Remove params.unit
- Update summary only with score
- Update details view with ms value
- Fix gauge background on chrome by also adding `-webkit-image-set(..)`
2023-03-07 22:03:09 +01:00
Brian Grinstead
24b094d1cc
Use performance.measure api instead of manually tracking with performance.now() (#79) 2023-02-27 12:53:55 -08:00
Thorsten Kober
d8bea6b8d4
remove labs (#80) 2023-02-23 13:32:10 -08:00
Thorsten Kober
bbec60788f
update readme for todoMVC apps (#78) 2023-02-21 15:45:19 -08:00
Camillo Bruni
e884d518b6
Fix the last broken test using _writeMark (#77) 2023-02-21 20:39:28 +01:00
Camillo Bruni
0644776001
Expose frozen globalThis.Suites (#72) 2023-02-21 20:10:16 +01:00
Camillo Bruni
c64ffc8eaa
Remove _writeMark test (#76)
The method has been removed since we're now using performance.now directly.
2023-02-21 20:06:57 +01:00
Camillo Bruni
661426329a
Use params everywhere (#69)
- Add support for filtering multiple suites
- Support startAutomatically in the default runner
- Fix displaying detailed results with unit=ms
- More error messages on invalid url-params
- Add Suites.enable function instead of two separate implementations per runner
2023-02-21 20:05:28 +01:00
Camillo Bruni
cb6cc0652f
Update titles to "Speedometer 3.0" (#75) 2023-02-21 19:56:47 +01:00
Camillo Bruni
d309edfe9d
Minor runner improvements (#74)
- Directly use performance.now and performance.mark
- Reduce a bit of overhead
- Use separate variables for sync / async time measurements to make the code more clear
- Optimize _appendFrame() code by using local variables
2023-02-21 19:52:51 +01:00
Camillo Bruni
207416957e
Add --highlight color CSS variable (#73) 2023-02-21 19:45:00 +01:00
Camillo Bruni
f4eb1ebaa5
Use 2x logo and gauge images (#70)
Make sure we load the hi-res background images in all cases:
- Use srcset for logo
- Use image-set for gauge CSS
2023-02-19 20:48:03 +01:00
Thorsten Kober
bf927da6ba
Remove wait from selenium test (#67) 2023-02-13 19:36:18 -08:00
Thorsten Kober
2fdc20cc2e
Adding prefer-template rule to Eslint (#66)
* run format

* eslint prefer-template

* re-format spacing

* fix string concat

* pr feedback

* rebase main
2023-02-13 11:54:51 -08:00
Camillo Bruni
f20363a3c4
Add buttons to download and copy JSON results (#65)
- Add "Download JSON" button to details view
- Add "Copy JSON" button to details view
- Example JSON result file name: "speedometer_3-2023-02-13T09_18_32.318Z.json"
- Update CSS formatting to make details button all the same width
- Add "title" to results buttons (both summary and details)
2023-02-13 20:44:44 +01:00
Camillo Bruni
1b06e553f9
Use GrandPrix Metric model (#63)
- Use more explicit Metric objects with parent and child-metrics
- Update statistics.mjs to use more ES6 features
2023-02-13 09:44:04 +01:00
Camillo Bruni
7722c8ab06
Prepare layout for more complex details view (#62)
- Use buttons for "about" links
- Make sure about text is readable on all view sizes (adding optional scrollbar)
- Add button-row to allow for more complex buttons in the upcoming details view (see PR "Use Metrics classes from GrandPrix" #42 )
- Add `--foreground-color` and `--text-width` CSS variables
- Hide screen-size warning on startup to avoid flickering
- Fix main view centering by using more detailed calc(...)
2023-02-11 13:35:30 +01:00
Brian Grinstead
0b592ff361
Update LICENSE (#64) 2023-02-10 20:09:27 -08:00
Camillo Bruni
6fcb6e6359
Make progress labels more readable (#60)
- Move current suite label to the left
- Move suite progress to the right
2023-02-09 10:44:10 +01:00
Camillo Bruni
76f4bb1e86
Use Score instead of Runs / Minute (#61)
RPM has always been a bit a confusing term in Speedometer2 as it was never real rpms, rather a scaled value.
To avoid future confusion it is renamed to the more neutral term "Score".
2023-02-09 09:42:13 +01:00
Thorsten Kober
4c22ba45f6
Add Eslint & Prettier (#57) 2023-02-08 13:53:53 -08:00
Camillo Bruni
480c9cee0f
Add command line options to tests/run.mjs (#58)
- Add basic command line options
- Improve error messages in case $BROWSER env variables isn't set
2023-02-07 22:08:31 +01:00
Camillo Bruni
00db41cea3
Add customisable viewport size (#41)
- Introduce params.mjs module that encodes all benchmarking parameters
- Change CSS to rely on --viewport-width and --viewport-height variables for easier resizing
- Update code and tests to rely on the newly added Params object
2023-02-07 22:07:02 +01:00
Thorsten Kober
42b6dd8a8d
Dev setup (#55) 2023-01-30 14:52:46 -08:00
Brian Grinstead
dbff607c76
cast iterationCount from url to number in interactive runner. Fixes #46 (#48) 2023-01-30 09:42:32 -08:00
Brian Grinstead
9566af6fe6
Run mocha test in CI using selenium (#45) 2023-01-26 14:56:57 -08:00
Brian Grinstead
69d5d5bf75
Remove the following tests, as decided in issue #3: (#49)
- EmberJS-Debug
- AngularJS (1)
- Inferno
- Elm
- Flight
2023-01-26 13:37:14 -08:00
Daniel Holbert
c0a1bc139b
Fix typo in CSS property name, in InteractiveRunner.html (#47)
InteractiveRunner.html has a CSS rule which includes a "list-position" property, which is not actually the name of a property.

The intent was surely to use "list-style-position".  Let's just correct it to that.
2023-01-26 13:01:20 -08:00
Houssein Djirdeh
adc3cca860
Adds unit tests for BenchmarkRunner using Mocha (#40) 2023-01-26 09:06:59 -08:00
Doug Thayer
2550aebedb
Unbreak react-stockcharts tests (#44)
I blundered my style fixups and unthinkingly pushed them up without
proper testing.
2023-01-25 22:15:05 -08:00
Doug Thayer
ed6143a8cc
Add react-stockcharts benchmarks (#11)
* Add react-stockcharts tests

* fixup React-Stockcharts for new runner

* Add back React-Stockcharts-SVG

* Added react-stockcharts under tentative directory

* Fix code style of react-stockcharts tests

* Fix up loop index variables
2023-01-25 12:26:17 -08:00
Camillo Bruni
ef8d58c988
Fix interactive runner (#31)
* Make the client callbacks async
* Move the interactive runner into a separate interactive.mjs module
* Update code to use URLSearchParams instead of custom parsing
2023-01-25 13:10:55 +01:00
Camillo Bruni
dce4dfeaa0
Add more Page methods and PageElement (#26)
Migrate all test code to go through Page or PageElement.

- Page defines a fixed interface to interact with a test frame
- PageElement defines a fixed interface to interact with Elements from an iframe

Cleanups:
- Move app initialization and custom code to the specific index.html files
- Update `event.initEvent(...)` code to use the recommended `Event` constructor
- Firefox still requires `initEvent` for the submit events
2022-11-30 19:24:46 +01:00
Camillo Bruni
714b5b4cb2
move methods (#29)
Sort BenchmarRunner run methods in chronological order.
2022-11-23 22:51:52 +01:00
Camillo Bruni
731c9e871d
Async BenchmarkRunner and removing BenchmarkState (#27)
Replace the continuation-based test running with an async-await loop over all suites and tests.
This also removes the necessity of the BenchmarkState class.
2022-11-23 21:07:54 +01:00
Camillo Bruni
e1125d91bc
Add Page object (#23)
Goal: Add a fixed interface to interact with test frames and their DOM nodes.
This will allow us to run more complex scenarios in the future without sacrificing flexibility or performance for the existing tests.

- Add `Page` object
- Refactor tests to interact with the frame only through the Page object
2022-11-22 13:59:43 +01:00
Brian Grinstead
52998e962d
Add development notice (#25) 2022-11-03 16:24:07 -07:00
Camillo Bruni
b840b30f7a
Remove flight-js mailclient (#22)
Remove the flightjs-mailclient app that was not included in the main Suites list.
2022-10-31 18:05:57 +01:00
Camillo Bruni
8787833334
Convert to ES6 Modules (#19)
- Rename files from .js to .mjs (not strictly necessary but it makes clear that we use modules)
- Pull in missing statistics.js file from https://github.com/WebKit/WebKit/blob/main/PerformanceTests/resources/statistics.js
- Restructure UI init code to be all in main.mjs
- Moved free-floating UI code to MainBenchmarkClient
- Added `_isRunning` instance variable to MainBenchmarkClient
- Fix CSS to address removed links
- Use `<button>` instead of links for better keyboard navigation in all browsers
2022-10-31 10:28:25 +01:00
Ryosuke Niwa
1e6af3de5f
Fix indentation after class conversion. (#18) 2022-09-19 10:54:38 -07:00
Camillo Bruni
281b94e7ae
Use ES6 Classes (#17) 2022-09-16 16:05:22 -07:00
Camillo Bruni
694d89c47e
Basic ES6 conversions: let/const and arrow functions (#16)
This is the first, format and function preserving PR to start the ES6.

- Convert var to let/const
- Use arrow functions where possible
2022-09-16 09:53:24 +02:00
Ryosuke Niwa
600ccf0e64
Delete SimplePromise and use regular native Promise. (#14)
Also deleted the unused _waitAndWarmUp.
2022-08-26 13:04:25 -07:00
Brian Grinstead
9bae5f5ef1
Add readme with goals for the benchmark (#13) 2022-08-23 20:37:48 -07:00
Ryosuke Niwa
3ceae4f0e9
Add Speedometer Governance Policy (#10) 2022-07-28 20:09:12 -07:00
Ryosuke Niwa
6b63846bad
Reviewed by Addy Osmani and Jonathan Bedard. (#2)
Updated the copyright.

Apple started contribution in 2013: http://commits.webkit.org/r156073.
Google contributed in 2017: https://commits.webkit.org/r216706 through https://commits.webkit.org/r222534.

* LICENSE
2022-06-29 11:40:19 -07:00
Ryosuke Niwa
a1e842164b
Add Speedometer 2.1 content. (#1)
Reviewed by Yusuke Suzuki, Addy Osmani, and Julian Descottes.

* InteractiveRunner.html: Added.
* index.html Added.
* resources: Added.
2022-06-06 08:50:03 -07:00
2523 changed files with 497481 additions and 1 deletions

48
.eslintignore Normal file
View file

@ -0,0 +1,48 @@
/node_modules
**/dist
package-lock.json
**/.angular
/resources/todomvc/architecture-examples/*
!/resources/todomvc/architecture-examples/angular
!/resources/todomvc/architecture-examples/angular-complex
!/resources/todomvc/architecture-examples/backbone
!/resources/todomvc/architecture-examples/backbone-complex
!/resources/todomvc/architecture-examples/lit
/resources/todomvc/architecture-examples/lit/.wireit
/resources/todomvc/architecture-examples/lit/dist/index.js
!/resources/todomvc/architecture-examples/lit-complex
!/resources/todomvc/architecture-examples/preact
!/resources/todomvc/architecture-examples/preact-complex
!/resources/todomvc/architecture-examples/react
!/resources/todomvc/architecture-examples/react-complex
!/resources/todomvc/architecture-examples/react-redux
!/resources/todomvc/architecture-examples/react-redux-complex
!/resources/todomvc/architecture-examples/svelte
!/resources/todomvc/architecture-examples/svelte-complex
!/resources/todomvc/architecture-examples/vue
!/resources/todomvc/architecture-examples/vue-complex
/resources/todomvc/vanilla-examples/*
!/resources/todomvc/vanilla-examples/javascript-es5
!/resources/todomvc/vanilla-examples/javascript-es5-complex
!/resources/todomvc/vanilla-examples/javascript-es6
!/resources/todomvc/vanilla-examples/javascript-es6-complex
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
!/resources/todomvc/vanilla-examples/javascript-web-components
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
!/resources/todomvc/big-dom-generator
!/resources/editors
/resources/editors/assets
!/resources/newssite
!/resources/newssite/news-next
!/resources/newssite/news-nuxt
!/resources/newssite/news-site-css
/resources/perf.webkit.org
/resources/react-stockcharts

101
.eslintrc Normal file
View file

@ -0,0 +1,101 @@
/*
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
{
"root": true,
"extends": ["eslint:recommended", "plugin:@typescript-eslint/base"],
"env": {
"browser": true,
"node": true,
"worker": true,
"es2022": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module"
},
"globals": {
// unit test specific
"sinon": true,
"expect": true,
// injected dependencies
"testRunner": true,
"startBenchmark": true,
"PerfTestRunner": true,
"deltas": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
"block-spacing": [2, "always"],
"brace-style": 2,
"comma-dangle": [2, "only-multiline"],
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"consistent-return": 2,
"curly": ["error", "multi-or-nest", "consistent"],
"dot-notation": 0,
"eol-last": 2,
"eqeqeq": 2,
"indent": ["error", 4, { "SwitchCase": 1 }],
"func-call-spacing": [2, "never"],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"keyword-spacing": [2, { "before": true, "after": true }],
"new-cap": 0,
"new-parens": 0,
"no-console": 0,
"no-constant-condition": 0,
"no-extra-bind": 2,
"no-extra-parens": [2, "all", { "nestedBinaryExpressions": false }],
"no-extra-semi": 2,
"no-global-assign": 2,
"no-inner-declarations": 0,
"no-multi-spaces": 2,
"no-multiple-empty-lines": ["error", { "max": 1 }],
"no-new": 0,
"no-proto": 0,
"no-redeclare": 0,
"no-return-assign": 2,
"no-return-await": 2,
"no-self-compare": 2,
"no-shadow": 0,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-underscore-dangle": 0,
"no-unused-expressions": 2,
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
"no-use-before-define": 0,
"nonblock-statement-body-position": ["error", "below"],
"object-curly-spacing": ["error", "always"],
"operator-linebreak": ["error", "before"],
"prefer-template": 2,
"quotes": [2, "double", { "avoidEscape": true }],
"semi": 2,
"semi-spacing": [2, { "before": false, "after": true }],
"space-infix-ops": 2,
"spaced-comment": [2, "always"],
"strict": 0,
"valid-typeof": 2
}
}

75
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,75 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
linters:
name: Linters
runs-on: macos-latest
steps:
- name: Checkout Branch
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: "npm"
- name: Install Node Packages
run: npm ci
- name: Run linters
run: npm run format
- name: Check if anything changed
run: |
git_status="`LC_ALL=C git status --porcelain --ignore-submodules -unormal 2>&1`"
if [ -n "$git_status" ]; then
printf "Some file(s) changed as the result of formatting, this means that you need to run the formatter on your patch.\n"
printf "Here is what changed:\n"
printf -- "$git_status\n\n"
printf "And here is the diff:\n"
git diff -U8
exit 1
fi
build:
name: Build
runs-on: macos-latest
strategy:
matrix:
browser: [chrome, firefox, safari]
steps:
- name: Install Firefox
if: ${{ matrix.browser == 'firefox' }}
run: brew install --cask firefox
- name: Checkout Branch
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: "npm"
- name: Install Node Packages
run: npm ci
- name: Run Unit Tests
run: |
echo "Running in $BROWSER"
npm run test:${{ matrix.browser }}
- name: Run end2end Tests
run: |
echo "Running in $BROWSER"
npm run test-e2e:${{ matrix.browser }}

33
.gitignore vendored
View file

@ -2,3 +2,36 @@
*.pyc
.DS_Store
.directory
/node_modules
.vscode
.angular
/resources/todomvc/architecture-examples/angular/node_modules/
/resources/todomvc/architecture-examples/angular-complex/node_modules/
/resources/todomvc/architecture-examples/backbone/node_modules/
/resources/todomvc/architecture-examples/backbone-complex/node_modules/
/resources/todomvc/architecture-examples/jquery/node_modules/
/resources/todomvc/architecture-examples/jquery-complex/node_modules/
/resources/todomvc/architecture-examples/preact/node_modules/
/resources/todomvc/architecture-examples/preact-complex/node_modules/
/resources/todomvc/architecture-examples/react/node_modules/
/resources/todomvc/architecture-examples/react-complex/node_modules/
/resources/todomvc/architecture-examples/react-redux/node_modules/
/resources/todomvc/architecture-examples/react-redux-complex/node_modules/
/resources/todomvc/architecture-examples/svelte/node_modules/
/resources/todomvc/architecture-examples/svelte-complex/node_modules/
/resources/todomvc/architecture-examples/vue/node_modules/
/resources/todomvc/architecture-examples/vue-complex/node_modules/
/resources/todomvc/architecture-examples/lit-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-es5/node_modules/
/resources/todomvc/vanilla-examples/javascript-es5-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6-webpack/node_modules/
/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/node_modules/
/resources/todomvc/vanilla-examples/javascript-web-components-complex/node_modules/
/resources/charts/node_modules/
/resources/todomvc/big-dom-generator/node_modules/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict = true

48
.prettierignore Normal file
View file

@ -0,0 +1,48 @@
/node_modules
**/dist
package-lock.json
**/.angular
/resources/todomvc/architecture-examples/*
!/resources/todomvc/architecture-examples/angular
!/resources/todomvc/architecture-examples/angular-complex
!/resources/todomvc/architecture-examples/backbone
!/resources/todomvc/architecture-examples/backbone-complex
!/resources/todomvc/architecture-examples/lit
/resources/todomvc/architecture-examples/lit/dist/index.js
/resources/todomvc/architecture-examples/lit/.wireit
!/resources/todomvc/architecture-examples/lit-complex
!/resources/todomvc/architecture-examples/preact
!/resources/todomvc/architecture-examples/preact-complex
!/resources/todomvc/architecture-examples/react
!/resources/todomvc/architecture-examples/react-complex
!/resources/todomvc/architecture-examples/react-redux
!/resources/todomvc/architecture-examples/react-redux-complex
!/resources/todomvc/architecture-examples/svelte
!/resources/todomvc/architecture-examples/svelte-complex
!/resources/todomvc/architecture-examples/vue
!/resources/todomvc/architecture-examples/vue-complex
/resources/todomvc/vanilla-examples/*
!/resources/todomvc/vanilla-examples/javascript-es5
!/resources/todomvc/vanilla-examples/javascript-es5-complex
!/resources/todomvc/vanilla-examples/javascript-es6
!/resources/todomvc/vanilla-examples/javascript-es6-complex
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
!/resources/todomvc/vanilla-examples/javascript-web-components
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
!/resources/todomvc/big-dom-generator
!/resources/editors
/resources/editors/assets
!/resources/newssite
!/resources/newssite/news-next
!/resources/newssite/news-nuxt
!/resources/newssite/news-site-css
/resources/perf.webkit.org
/resources/react-stockcharts

12
.prettierrc Normal file
View file

@ -0,0 +1,12 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"printWidth": 250,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "es5",
"useTabs": false
}

34
Development.md Normal file
View file

@ -0,0 +1,34 @@
# Development Environment
## Prerequisite
Node is required for local development.
We recommend using a Node version manager ([nvm](https://github.com/nvm-sh/nvm) for example) to ensure a stable development environment is used.
Below are the required node / npm versions:
```sh
"node": ">18.13.0",
"npm": ">8.19.3"
```
## Installation
In your working directory, open terminal and paste the following commands:
```sh
git clone https://github.com/WebKit/Speedometer.git
cd Speedometer
npm install
```
## Run Development Server
1. In your terminal run:
```sh
PORT=8900 npm run dev # Passing the env var PORT is optional
```
2. Open your browser of choice and navigate to [http://127.0.0.1:8900](http://127.0.0.1:8900)
## Local Server
Speedometer uses [http-server](https://github.com/http-party/http-server), which is a static HTTP server. Meaning it does not provide hot-reloading. By default, the dev script disables caching and local changes can be viewed by simply refreshing your browser window. Additional options of the http-server can be found [here](https://github.com/http-party/http-server#available-options).

34
Governance.md Normal file
View file

@ -0,0 +1,34 @@
# Speedometer Governance Policy
Speedometer uses multistakeholder governance.
This will allow us to share work, and build a collaborative understanding of performance on the web
in order to drive resourcing towards appropriate areas.
This also provides a structure that can endure to provide maintenance and adapt to the future web.
An eligible “browser project” is a core end-to-end web browser engine with an integrated JavaScript engine
which distributes implementations widely. The project may delegate decision making within Speedometer
to multiple representatives (for example, to review code commits or to provide consensus for major changes).
The participating browser projects at this time are Blink/V8, Gecko/SpiderMonkey, and WebKit/JavaScriptCore.
The intent is that the working team should be able to move quickly for most changes,
with a higher level of process and consensus expected based on the impact of the change.
- **Trivial change** - This is a change that has no effect on the official benchmark and includes changes
to whitespaces, comments, documentation outside policies and governance model, and unofficial test cases.
A trivial change requires approval by a reviewer, who is not the author of the change,
from one of the participating browser projects.
The intent is to ensure basic code quality & license compatibility, not to reach agreement.
For example, one participating browser project might be both writing and reviewing a new benchmark in
a subfolder to test in their own CI, or reviewing code written by an external contributor.
- **Non-trivial change** - This is a change that has small impact on the official benchmark and includes
changes to official test cases, test runners, bug fixes, and the appearance of the benchmark.
A non-trivial change requires approval by at least two of the participating browser projects
(including either authoring or reviewing the change) and none other strongly opposed to the change
within 10 business days.
- **Major change** - This is a change that has major implications on the official benchmark such as
releasing of a new version of the benchmark or any revisions to governance policies and processes,
including changes to the participating browser projects.
A major change requires a consensus, meaning approvals by each of the participating browser projects.
This governance policy and associated code will be hosted inside the Speedometer repository within
the WebKit GitHub organization under the 2-clause BSD license.

View file

@ -1,4 +1,7 @@
Copyright (C) 2022 Apple Inc. All rights reserved.
Copyright (C) 2013-2024 Apple Inc. All rights reserved.
Copyright (C) 2017-2024 Google Inc. All rights reserved.
Copyright (C) 2022-2024 Mozilla Corporation. All rights reserved.
Copyright (C) 2023-2024 Microsoft Corporation. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# What is Speedometer?
Speedometer is a benchmark for web browsers that measures Web application responsiveness
by timing simulated user interactions on various workloads. Our primary goal is to make it
reflect the real-world Web as much as possible. When a browser improves its score on the
benchmark, actual users should benefit. In order to achieve this, it should:
- Test end-to-end user journeys instead of testing specific features in a tight loop. Each
test should exercise the full set of what’s needed from the engine in order for a user to
accomplish a task.
- Evolve over time, adapting to the present Web on a regular basis. This should be informed
by current usage data, and by consensus about features which are important for engines to
optimize to provide a consistent experience for users and site authors.
- Be accessible to the public and useful to browser engineers. It should run in every modern
browser by visiting a normal web page. It should run relatively quickly, while providing
enough test coverage to be reflective of the real-world Web.
# Score and Test Measurement
Each test can contain several steps who contributed to the test duration. Note that the prepare step of a test is _unmeasured_ and thus does not contribute to the score. All following test
steps are **measured** and summed up in the test step time.
<img src="./resources/measurement-timeline.svg" />
Each step consists of the following phases:
<dl>
<dt>Sync:</dt>
<dd>Time spent in synchronous JS execution.</dd>
<dt>Async:</dt>
<dd>Time spent in asynchronous JS like Promise or setTimeout tasks scheduled by the sync-step.</dd>
</dl>
## Score
The final benchmark score is calculated based of the inverse of [geomean](https://en.wikipedia.org/wiki/Geometric_mean) of all tests. We average the score over multiple iterations with the [arithmetic mean](https://en.wikipedia.org/wiki/Arithmetic_mean). By using the geomean to combine the test durations we maintain the invariant that relative improvements are favoured equally amongst all tests, even though they can have vastly different durations.
<img src="./resources/score.svg" />

29
Testing.md Normal file
View file

@ -0,0 +1,29 @@
# Testing
Speedometer uses [Selenium](https://www.selenium.dev/) for testing of the application itself.
Tests are located in the `/tests` folder.
[Sinon](https://sinonjs.org/): Standalone test spies, stubs and mocks for JavaScript.
[Mocha](https://mochajs.org/): Testing framework.
## Local Testing
To run this locally you'll need the browsers installed along with the corresponding driver:
- [chromedriver](https://chromedriver.chromium.org/getting-started)
- [geckodriver](https://github.com/mozilla/geckodriver/releases)
- [safaridriver](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari)
- [msedgedriver](https://developer.microsoft.com/microsoft-edge/tools/webdriver).
Once installed you can run the following scripts:
```bash
npm run test:chrome
npm run test:firefox
npm run test:safari
npm run test:edge
```
## Automated Testing
Currently Speedometer's tests run automatically, when pushing to the main branch or when opening a pr.

105
about.html Normal file
View file

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer 3.0 About</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
</head>
<body>
<main>
<section id="about" class="visible" data-title="About">
<a href="./" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="section-grid">
<h1 class="section-header">About Speedometer 3</h1>
<div class="section-content">
<p>Speedometer 3 is a benchmark for web browsers that measures Web application responsiveness by timing simulated user interactions on various workloads.</p>
<p>
It's developed as an open source project, with decisions being made under a <a href="https://github.com/WebKit/Speedometer/blob/main/Governance.md">multistakeholder governance model</a> between the three widely distributed
web browser engine projects.
</p>
<p>
The following high level user journeys are implemented in the current version. Each of these journeys has one or more workloads which test important aspects of it — for example commonly used patterns, frameworks, or
technologies.
</p>
<ul>
<li>Working with a todo list</li>
<ul>
<li>Measures the time to add, complete, and remove 100 todo items in a basic list.</li>
<li>Each example implements the same todo application (TodoMVC) using different techniques and frameworks.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es5" target="_blank">TodoMVC-JavaScript-ES5</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex" target="_blank">TodoMVC-JavaScript-ES6-Webpack-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-web-components" target="_blank">TodoMVC-WebComponents</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-complex" target="_blank">TodoMVC-React-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-redux" target="_blank">TodoMVC-React-Redux</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/backbone" target="_blank">TodoMVC-Backbone</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/angular-complex" target="_blank">TodoMVC-Angular-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/vue" target="_blank">TodoMVC-Vue</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/jquery" target="_blank">TodoMVC-jQuery</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/preact-complex" target="_blank">TodoMVC-Preact-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/svelte-complex" target="_blank">TodoMVC-Svelte-Complex-DOM</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/lit-complex" target="_blank">TodoMVC-Lit-Complex-DOM</a>
</li>
</ul>
<li>Editing rich text</li>
<ul>
<li>Loading and styling text inside WYSIWYG and code editors.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-CodeMirror</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-TipTap</a>
</li>
</ul>
<li>Rendering charts</li>
<ul>
<li>Loading and interacting with SVG and canvas charts.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-observable-plot</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-chartjs</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/react-stockcharts" target="_blank">React-Stockcharts-SVG</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/perf.webkit.org" target="_blank">Perf-Dashboard</a>
</li>
</ul>
<li>Reading a news site</li>
<ul>
<li>Navigating across pages and interacting with a typical looking news site.</li>
<li>
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-next" target="_blank">NewsSite-Next</a>,
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-nuxt" target="_blank">NewsSite-Nuxt</a>
</li>
</ul>
</ul>
<p class="note"><strong>Notes about methodology</strong></p>
<ul>
<li>Although user-driven actions like mouse movements and keyboard input cannot be fully emulated in JavaScript, Speedometer does its best to faithfully replay a typical workload within the demo applications.</li>
<li>To make the run time long enough to measure with the limited precision, we synchronously execute a large number of the operations, such as adding one hundred to-do items.</li>
<li>
Modern browser engines execute some work asynchronously as an optimization strategy to reduce the run time of synchronous operations. While returning control back to JavaScript execution as soon as possible is worth
pursuing, the run time cost of such an asynchronous work should still be taken into a holistic measurement of web application performance. In addition, some JavaScript frameworks call into DOM APIs asynchronously as an
optimization technique. Speedometer approximates the run time of this asynchronous work in the UI thread with a zero-second timer that is scheduled immediately after each execution of synchronous operations.
</li>
<li>Speedometer does not attempt to measure concurrent asynchronous work (e.g. in Web Workers).</li>
<li>Speedometer should not be used as a way to compare the performance of different JavaScript frameworks.</li>
<li>
The goal of all workloads is to represent a scenario that could be found on the Web. Although all workloads strive to use patterns that are commonly used, some implementation details are Speedometer specific and should
not be used as a guideline on how to implement and deploy a standalone app. For example, due to constraints within the test harness, workloads must not depend on a server infrastructure to function properly and are
built as static files ahead of time.
</li>
</ul>
</div>
<div class="buttons section-footer">
<div class="button-row">
<a class="button" href="./" title="Show main section.">Home</a>
<a class="button" href="instructions.html" title="Show test instructions.">Test Instructions</a>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

10
babel.config.json Normal file
View file

@ -0,0 +1,10 @@
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}

View file

@ -0,0 +1,42 @@
# Speedometer 3.0: TodoMVC: JavaScript Es5
## Description
This application uses JavaScript with ES5 language features to implement a todo application.
JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. While it is most well-known as the scripting language for Web pages, many non-browser environments also use it..
[JavaScript - developer.mozilla.org](http://developer.mozilla.org/en-US/docs/JavaScript)
## Implementation details
This implementation uses an explicit MVC pattern, with a clear file structure to reflect the architecture. The storage solution uses an in-memory data object that implements a simple array to hold the todos.
## Built steps
A simple build script copies all necessary files to a `dist` folder.
It does not rely on compilers or transpilers and serves raw html, css and js files to the user.
```
npm run build
```
## Requirements
The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server.
```
* Node (min version: 18.13.0)
* NPM (min version: 8.19.3)
```
## Local preview
```
terminal:
1. npm install
2. npm run dev
browser:
1. http://localhost:7001/
```

View file

@ -0,0 +1,25 @@
/* global app, $on */
(function () {
"use strict";
/**
* Sets up a brand new Todo list.
*
* @param {string} name The name of your new to do list.
*/
function Todo(name) {
this.storage = new app.Store(name);
this.model = new app.Model(this.storage);
this.template = new app.Template();
this.view = new app.View(this.template);
this.controller = new app.Controller(this.model, this.view);
}
var todo = new Todo("javascript-es5");
function setView() {
todo.controller.setView(document.location.hash);
}
$on(window, "load", setView);
$on(window, "hashchange", setView);
})();

View file

@ -0,0 +1,141 @@
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}

View file

@ -0,0 +1,266 @@
(function (window) {
"use strict";
/**
* Takes a model and view and acts as the controller between them
*
* @constructor
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var self = this;
self.model = model;
self.view = view;
self.view.bind("newTodo", function (title) {
self.addItem(title);
});
self.view.bind("itemEdit", function (item) {
self.editItem(item.id);
});
self.view.bind("itemEditDone", function (item) {
self.editItemSave(item.id, item.title);
});
self.view.bind("itemEditCancel", function (item) {
self.editItemCancel(item.id);
});
self.view.bind("itemRemove", function (item) {
self.removeItem(item.id);
});
self.view.bind("itemToggle", function (item) {
self.toggleComplete(item.id, item.completed);
});
self.view.bind("removeCompleted", function () {
self.removeCompletedItems();
});
self.view.bind("toggleAll", function (status) {
self.toggleAll(status.completed);
});
}
/**
* Loads and initialises the view
*
* @param {string} '' | 'active' | 'completed'
*/
Controller.prototype.setView = function (locationHash) {
var route = locationHash.split("/")[1];
var page = route || "";
this._updateFilterState(page);
};
/**
* An event to fire on load. Will get all items and display them in the
* todo-list
*/
Controller.prototype.showAll = function () {
var self = this;
self.model.read(function (data) {
self.view.render("showEntries", data);
});
};
/**
* Renders all active tasks
*/
Controller.prototype.showActive = function () {
var self = this;
self.model.read({ completed: false }, function (data) {
self.view.render("showEntries", data);
});
};
/**
* Renders all completed tasks
*/
Controller.prototype.showCompleted = function () {
var self = this;
self.model.read({ completed: true }, function (data) {
self.view.render("showEntries", data);
});
};
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
Controller.prototype.addItem = function (title) {
var self = this;
if (title.trim() === "")
return;
self.model.create(title, function () {
self.view.render("clearNewTodo");
self._filter(true);
});
};
/*
* Triggers the item editing mode.
*/
Controller.prototype.editItem = function (id) {
var self = this;
self.model.read(id, function (data) {
self.view.render("editItem", { id: id, title: data[0].title });
});
};
/*
* Finishes the item editing mode successfully.
*/
Controller.prototype.editItemSave = function (id, title) {
var self = this;
title = title.trim();
if (title.length !== 0) {
self.model.update(id, { title: title }, function () {
self.view.render("editItemDone", { id: id, title: title });
});
} else {
self.removeItem(id);
}
};
/*
* Cancels the item editing mode.
*/
Controller.prototype.editItemCancel = function (id) {
var self = this;
self.model.read(id, function (data) {
self.view.render("editItemDone", { id: id, title: data[0].title });
});
};
/**
* By giving it an ID it'll find the DOM element matching that ID,
* remove it from the DOM and also remove it from storage.
*
* @param {number} id The ID of the item to remove from the DOM and
* storage
*/
Controller.prototype.removeItem = function (id) {
var self = this;
self.model.remove(id, function () {
self.view.render("removeItem", id);
});
self._filter();
};
/**
* Will remove all completed items from the DOM and storage.
*/
Controller.prototype.removeCompletedItems = function () {
var self = this;
self.model.read({ completed: true }, function (data) {
data.forEach(function (item) {
self.removeItem(item.id);
});
});
self._filter();
};
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
Controller.prototype.toggleComplete = function (id, completed, silent) {
var self = this;
self.model.update(id, { completed: completed }, function () {
self.view.render("elementComplete", {
id: id,
completed: completed,
});
});
if (!silent)
self._filter();
};
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
Controller.prototype.toggleAll = function (completed) {
var self = this;
self.model.read({ completed: !completed }, function (data) {
data.forEach(function (item) {
self.toggleComplete(item.id, completed, true);
});
});
self._filter();
};
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
Controller.prototype._updateCount = function () {
var self = this;
self.model.getCount(function (todos) {
self.view.render("updateElementCount", todos.active);
self.view.render("clearCompletedButton", {
completed: todos.completed,
visible: todos.completed > 0,
});
self.view.render("toggleAll", { checked: todos.completed === todos.total });
self.view.render("contentBlockVisibility", { visible: todos.total > 0 });
});
};
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
Controller.prototype._filter = function (force) {
var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]();
if (force || this._lastActiveRoute !== "All" || this._lastActiveRoute !== activeRoute)
this[`show${activeRoute}`]();
this._lastActiveRoute = activeRoute;
};
/**
* Simply updates the filter nav's selected states
*/
Controller.prototype._updateFilterState = function (currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === "")
this._activeRoute = "All";
this._filter();
this.view.render("setFilter", currentPage);
};
// Export to window
window.app = window.app || {};
window.app.Controller = Controller;
})(window);

View file

@ -0,0 +1,51 @@
/* global NodeList */
(function (window) {
"use strict";
// Get element(s) by CSS selector:
window.qs = function (selector, scope) {
return (scope || document).querySelector(selector);
};
window.qsa = function (selector, scope) {
return (scope || document).querySelectorAll(selector);
};
// addEventListener wrapper:
window.$on = function (target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
};
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
window.$delegate = function (target, selector, type, handler) {
function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = window.qsa(selector, target);
var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
if (hasMatch)
handler.call(targetElement, event);
}
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === "blur" || type === "focus";
window.$on(target, type, dispatchEvent, useCapture);
};
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div');
window.$parent = function (element, tagName) {
if (!element.parentNode)
return undefined;
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase())
return element.parentNode;
return window.$parent(element.parentNode, tagName);
};
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
})(window);

View file

@ -0,0 +1,393 @@
@charset "utf-8";
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 400;
color: rgba(0, 0, 0, 0.4);
}
.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
height: 65px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 65px;
font-size: 0;
position: absolute;
top: -65px;
left: -0;
}
.toggle-all + label:before {
content: '❯';
display: inline-block;
font-size: 22px;
color: #949494;
padding: 10px 27px 10px 27px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all:checked + label:before {
color: #484848;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #484848;
}
.todo-list li.completed label {
color: #949494;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #949494;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover,
.todo-list li .destroy:focus {
color: #C18585;
}
.todo-list li .destroy:after {
content: '×';
display: block;
height: 100%;
line-height: 1.1;
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: #DB7676;
}
.filters li a.selected {
border-color: #CE4646;
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 19px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
:focus,
.toggle:focus + label,
.toggle-all:focus + label {
box-shadow: 0 0 2px 2px #CF7D7D;
outline: 0;
}

View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en" data-framework="javascript" data-version="es5" data-features="">
<head>
<meta charset="UTF-8" />
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TodoMVC: JavaScript Es5</title>
<link rel="stylesheet" href="base.css" />
<link rel="stylesheet" href="index.css" />
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus />
</header>
<main class="main">
<div class="toggle-all-container">
<input class="toggle-all" type="checkbox" />
<label class="toggle-all-label" for="toggle-all">Mark all as complete</label>
</div>
<ul class="todo-list show-priority"></ul>
</main>
<footer class="footer">
<span class="todo-count"></span>
<ul class="filters">
<li>
<a href="#/" class="selected">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Click on input field to write your todo.</p>
<p>At least two characters are needed to be a valid entry.</p>
<p>Press 'enter' to add the todo.</p>
<p>Double-click to edit a todo</p>
</footer>
<script src="helpers.js"></script>
<script src="store.js"></script>
<script src="model.js"></script>
<script src="template.js"></script>
<script src="view.js"></script>
<script src="controller.js"></script>
<script src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,119 @@
(function (window) {
"use strict";
/**
* Creates a new Model instance and hooks up the storage.
*
* @constructor
* @param {object} storage A reference to the client side storage class
*/
function Model(storage) {
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
Model.prototype.create = function (title, callback) {
title = title || "";
callback = callback || function () {};
var newItem = {
title: title.trim(),
completed: false,
};
this.storage.save(newItem, callback);
};
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func); // Will find the model with an ID of 1
* model.read('1'); // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' });
*/
Model.prototype.read = function (query, callback) {
var queryType = typeof query;
callback = callback || function () {};
if (queryType === "function") {
callback = query;
return this.storage.findAll(callback);
} else if (queryType === "string" || queryType === "number") {
query = parseInt(query, 10);
return this.storage.find({ id: query }, callback);
} else {
return this.storage.find(query, callback);
}
};
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
Model.prototype.update = function (id, data, callback) {
this.storage.save(data, callback, id);
};
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
Model.prototype.remove = function (id, callback) {
this.storage.remove(id, callback);
};
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
Model.prototype.removeAll = function (callback) {
this.storage.drop(callback);
};
/**
* Returns a count of all todos
*/
Model.prototype.getCount = function (callback) {
var todos = {
active: 0,
completed: 0,
total: 0,
};
this.storage.findAll(function (data) {
data.forEach(function (todo) {
if (todo.completed)
todos.completed++;
else
todos.active++;
todos.total++;
});
callback(todos);
});
};
// Export to window
window.app = window.app || {};
window.app.Model = Model;
})(window);

View file

@ -0,0 +1,177 @@
/* jshint eqeqeq:false */
(function (window) {
"use strict";
var MemoryStorage = {
_data: {},
_name: "",
init: function (name, data = {}) {
this._name = name;
if (!this.getData())
this.setData(data);
},
getData: function () {
const data = this._data[this._name];
if (!data)
return null;
return JSON.parse(data);
},
setData: function (value) {
this._data[this._name] = JSON.stringify(value);
},
};
var LocalStorage = {
_name: "",
init: function (name, data = {}) {
this._name = name;
if (!this.getData())
this.setData(data);
},
getData: function () {
const data = window.localStorage.getItem(this._name);
if (!data)
return null;
return JSON.parse(data);
},
setData: function (value) {
window.localStorage.setItem(this._name, JSON.stringify(value));
},
};
var ID = 1;
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
function Store(name, callback, type = "disk") {
callback = callback || function () {};
this._dbName = name;
this.storage = type === "memory" ? MemoryStorage : LocalStorage;
this.storage.init(name, []);
callback.call(this, this.storage.getData());
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* });
*/
Store.prototype.find = function (query, callback) {
if (!callback)
return;
var todos = this.storage.getData();
callback.call(
this,
todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q])
return false;
}
return true;
})
);
};
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
Store.prototype.findAll = function (callback) {
callback = callback || function () {};
callback.call(this, this.storage.getData());
};
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
Store.prototype.save = function (updateData, callback, id) {
var todos = this.storage.getData();
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
for (var key in updateData)
todos[i][key] = updateData[key];
break;
}
}
this.storage.setData(todos);
callback.call(this, todos);
} else {
// Generate an ID
updateData.id = ID++;
todos.push(updateData);
this.storage.setData(todos);
callback.call(this, [updateData]);
}
};
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
Store.prototype.remove = function (id, callback) {
var todos = this.storage.getData();
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todos.splice(i, 1);
break;
}
}
this.storage.setData(todos);
callback.call(this, todos);
};
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
Store.prototype.drop = function (callback) {
var data = [];
this.storage.setData(data);
callback.call(this, data);
};
// Export to window
window.app = window.app || {};
window.app.Store = Store;
})(window);

View file

@ -0,0 +1,112 @@
/* jshint laxbreak:true */
(function (window) {
"use strict";
var htmlEscapes = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;",
};
var escapeHtmlChar = function (chr) {
return htmlEscapes[chr];
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function (string) {
return string && reHasUnescapedHtml.test(string) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string;
};
/**
* Sets up defaults for all the Template methods such as a default template
*
* @constructor
*/
function Template() {
this.defaultTemplate
= '<li data-id="{{id}}" class="{{completed}}" data-priority="{{priority}}">'
+ '<div class="view">'
+ '<input class="toggle" type="checkbox" {{checked}}>'
+ "<label>{{title}}</label>"
+ '<button class="destroy"></button>'
+ "</div>"
+ "</li>";
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* });
*/
Template.prototype.show = function (data) {
var i, l;
var view = "";
for (i = 0, l = data.length; i < l; i++) {
var template = this.defaultTemplate;
var completed = "";
var checked = "";
if (data[i].completed) {
completed = "completed";
checked = "checked";
}
template = template.replace("{{id}}", data[i].id);
template = template.replace("{{title}}", escape(data[i].title));
template = template.replace("{{completed}}", completed);
template = template.replace("{{checked}}", checked);
template = template.replace("{{priority}}", 4 - (i % 5));
view = view + template;
}
return view;
};
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
Template.prototype.itemCounter = function (activeTodos) {
var plural = activeTodos === 1 ? "" : "s";
return `<strong>${activeTodos}</strong> item${plural} left`;
};
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
Template.prototype.clearCompletedButton = function (completedTodos) {
if (completedTodos > 0)
return "Clear completed";
else
return "";
};
// Export to window
window.app = window.app || {};
window.app.Template = Template;
})(window);

View file

@ -0,0 +1,215 @@
/* global qs, qsa, $on, $parent, $delegate */
(function (window) {
"use strict";
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
function View(template) {
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = qs(".todo-list");
this.$todoItemCounter = qs(".todo-count");
this.$clearCompleted = qs(".clear-completed");
this.$main = qs(".main");
this.$footer = qs(".footer");
this.$toggleAllInput = qs(".toggle-all");
this.$toggleAll = qs(".toggle-all-label");
this.$newTodo = qs(".new-todo");
}
View.prototype._removeItem = function (id) {
var elem = qs(`[data-id="${id}"]`);
if (elem)
this.$todoList.removeChild(elem);
};
View.prototype._clearCompletedButton = function (completedCount, visible) {
this.$clearCompleted.textContent = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? "block" : "none";
};
View.prototype._setFilter = function (currentPage) {
qs(".filters .selected").className = "";
qs(`.filters [href="#/${currentPage}"]`).className = "selected";
};
View.prototype._elementComplete = function (id, completed) {
var listItem = qs(`[data-id="${id}"]`);
if (!listItem)
return;
listItem.className = completed ? "completed" : "";
// In case it was toggled from an event and not by clicking the checkbox
qs("input", listItem).checked = completed;
};
View.prototype._editItem = function (id, title) {
var listItem = qs(`[data-id="${id}"]`);
if (!listItem)
return;
listItem.className = `${listItem.className} editing`;
var input = document.createElement("input");
input.className = "edit";
listItem.appendChild(input);
input.focus();
input.value = title;
};
View.prototype._editItemDone = function (id, title) {
var listItem = qs(`[data-id="${id}"]`);
if (!listItem)
return;
var input = qs("input.edit", listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace("editing", "");
qsa("label", listItem).forEach(function (label) {
label.textContent = title;
});
};
View.prototype._replaceContentWithHtml = function (element, html) {
const parsedDocument = new DOMParser().parseFromString(html, "text/html");
element.replaceChildren(...parsedDocument.body.childNodes);
};
View.prototype.render = function (viewCmd, parameter) {
var self = this;
var viewCommands = {
showEntries: function () {
self._replaceContentWithHtml(self.$todoList, self.template.show(parameter));
},
removeItem: function () {
self._removeItem(parameter);
},
updateElementCount: function () {
self._replaceContentWithHtml(self.$todoItemCounter, self.template.itemCounter(parameter));
},
clearCompletedButton: function () {
self._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function () {
self.$main.style.display = self.$footer.style.display = parameter.visible ? "block" : "none";
},
toggleAll: function () {
self.$toggleAll.checked = parameter.checked;
},
setFilter: function () {
self._setFilter(parameter);
},
clearNewTodo: function () {
self.$newTodo.value = "";
},
elementComplete: function () {
self._elementComplete(parameter.id, parameter.completed);
},
editItem: function () {
self._editItem(parameter.id, parameter.title);
},
editItemDone: function () {
self._editItemDone(parameter.id, parameter.title);
},
};
viewCommands[viewCmd]();
};
View.prototype._itemId = function (element) {
var li = $parent(element, "li");
return parseInt(li.dataset.id, 10);
};
View.prototype._bindItemEditDone = function (handler) {
var self = this;
$delegate(self.$todoList, "li .edit", "blur", function () {
if (!this.dataset.iscanceled) {
handler({
id: self._itemId(this),
title: this.value,
});
}
});
$delegate(self.$todoList, "li .edit", "keypress", function (event) {
if (event.keyCode === self.ENTER_KEY) {
// Remove the cursor from the input when you hit enter just like if it
// were a real form
this.blur();
}
});
};
View.prototype._bindItemEditCancel = function (handler) {
var self = this;
$delegate(self.$todoList, "li .edit", "keyup", function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
this.dataset.iscanceled = true;
this.blur();
handler({ id: self._itemId(this) });
}
});
};
View.prototype.bind = function (event, handler) {
var self = this;
if (event === "newTodo") {
$on(self.$newTodo, "change", function () {
handler(self.$newTodo.value);
});
} else if (event === "removeCompleted") {
$on(self.$clearCompleted, "click", function () {
handler();
});
} else if (event === "toggleAll") {
$on(self.$toggleAll, "click", function () {
self.$toggleAllInput.click();
handler({ completed: self.$toggleAllInput.checked });
});
} else if (event === "itemEdit") {
$delegate(self.$todoList, "li label", "dblclick", function () {
handler({ id: self._itemId(this) });
});
} else if (event === "itemRemove") {
$delegate(self.$todoList, ".destroy", "click", function () {
handler({ id: self._itemId(this) });
});
} else if (event === "itemToggle") {
$delegate(self.$todoList, ".toggle", "click", function () {
handler({
id: self._itemId(this),
completed: this.checked,
});
});
} else if (event === "itemEditDone") {
self._bindItemEditDone(handler);
} else if (event === "itemEditCancel") {
self._bindItemEditCancel(handler);
}
};
// Export to window
window.app = window.app || {};
window.app.View = View;
})(window);

View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en" data-framework="javascript" data-version="es5" data-features="">
<head>
<meta charset="UTF-8" />
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TodoMVC: JavaScript Es5</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css" />
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css" />
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus />
</header>
<main class="main">
<div class="toggle-all-container">
<input class="toggle-all" type="checkbox" />
<label class="toggle-all-label" for="toggle-all">Mark all as complete</label>
</div>
<ul class="todo-list show-priority"></ul>
</main>
<footer class="footer">
<span class="todo-count"></span>
<ul class="filters">
<li>
<a href="#/" class="selected">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Click on input field to write your todo.</p>
<p>At least two characters are needed to be a valid entry.</p>
<p>Press 'enter' to add the todo.</p>
<p>Double-click to edit a todo</p>
</footer>
<script src="src/helpers.js"></script>
<script src="src/store.js"></script>
<script src="src/model.js"></script>
<script src="src/template.js"></script>
<script src="src/view.js"></script>
<script src="src/controller.js"></script>
<script src="src/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,787 @@
{
"name": "todomvc-javascript-es5",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "todomvc-javascript-es5",
"version": "1.0.0",
"dependencies": {
"todomvc-app-css": "^2.4.2",
"todomvc-common": "^1.0.5"
},
"devDependencies": {
"http-server": "^14.1.1"
},
"engines": {
"node": ">=18.13.0",
"npm": ">=8.19.3"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"dev": true,
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"dev": true,
"dependencies": {
"whatwg-encoding": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-server": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
"dev": true,
"dependencies": {
"basic-auth": "^2.0.1",
"chalk": "^4.1.2",
"corser": "^2.0.1",
"he": "^1.2.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy": "^1.18.1",
"mime": "^1.6.0",
"minimist": "^1.2.6",
"opener": "^1.5.1",
"portfinder": "^1.0.28",
"secure-compare": "3.0.1",
"union": "~0.5.0",
"url-join": "^4.0.1"
},
"bin": {
"http-server": "bin/http-server"
},
"engines": {
"node": ">=12"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true,
"bin": {
"opener": "bin/opener-bin.js"
}
},
"node_modules/portfinder": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
"dev": true,
"dependencies": {
"async": "^2.6.4",
"debug": "^3.2.7",
"mkdirp": "^0.5.6"
},
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"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/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
"dev": true
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/todomvc-app-css": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.4.2.tgz",
"integrity": "sha512-ViAkQ7ed89rmhFIGRsT36njN+97z8+s3XsJnB8E2IKOq+/SLD/6PtSvmTtiwUcVk39qPcjAc/OyeDys4LoJUVg=="
},
"node_modules/todomvc-common": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz",
"integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ=="
},
"node_modules/union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dev": true,
"dependencies": {
"qs": "^6.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"dev": true
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dev": true,
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
}
}
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"dev": true,
"requires": {
"safe-buffer": "5.1.2"
}
},
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
"dev": true
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
},
"follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"dev": true,
"requires": {
"whatwg-encoding": "^2.0.0"
}
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-server": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
"dev": true,
"requires": {
"basic-auth": "^2.0.1",
"chalk": "^4.1.2",
"corser": "^2.0.1",
"he": "^1.2.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy": "^1.18.1",
"mime": "^1.6.0",
"minimist": "^1.2.6",
"opener": "^1.5.1",
"portfinder": "^1.0.28",
"secure-compare": "3.0.1",
"union": "~0.5.0",
"url-join": "^4.0.1"
}
},
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"requires": {
"minimist": "^1.2.6"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"dev": true
},
"opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true
},
"portfinder": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
"dev": true,
"requires": {
"async": "^2.6.4",
"debug": "^3.2.7",
"mkdirp": "^0.5.6"
}
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dev": true,
"requires": {
"side-channel": "^1.0.4"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"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
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
"dev": true
},
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"todomvc-app-css": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.4.2.tgz",
"integrity": "sha512-ViAkQ7ed89rmhFIGRsT36njN+97z8+s3XsJnB8E2IKOq+/SLD/6PtSvmTtiwUcVk39qPcjAc/OyeDys4LoJUVg=="
},
"todomvc-common": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz",
"integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ=="
},
"union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dev": true,
"requires": {
"qs": "^6.4.0"
}
},
"url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"dev": true
},
"whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"dev": true,
"requires": {
"iconv-lite": "0.6.3"
}
}
}
}

View file

@ -0,0 +1,22 @@
{
"name": "todomvc-localstorage",
"version": "1.0.0",
"description": "TodoMVC app written with JavaScript es5 features.",
"engines": {
"node": ">=18.13.0",
"npm": ">=8.19.3"
},
"private": true,
"scripts": {
"dev": "http-server ./ -p 7001 -c-1 --cors",
"build": "node scripts/build.js",
"serve": "http-server ./dist -p 7002 -c-1 --cors"
},
"dependencies": {
"todomvc-app-css": "^2.4.2",
"todomvc-common": "^1.0.5"
},
"devDependencies": {
"http-server": "^14.1.1"
}
}

View file

@ -0,0 +1,56 @@
const fs = require("fs").promises;
const path = require("path");
const rootDirectory = "./";
const sourceDirectory = "./src";
const targetDirectory = "./dist";
const htmlFile = "index.html";
const filesToMove = ["node_modules/todomvc-common/base.css", "node_modules/todomvc-app-css/index.css"];
const copy = async (src, dest) => {
await fs.copyFile(src, dest);
};
const build = async () => {
// remove dist directory if it exists
await fs.rm(targetDirectory, { recursive: true, force: true });
// re-create the directory.
await fs.mkdir(targetDirectory);
// copy src folder
await fs.cp(sourceDirectory, targetDirectory, { recursive: true }, (err) => {
if (err)
console.error(err);
});
// copy html file
await fs.copyFile(path.join(rootDirectory, htmlFile), path.join(targetDirectory, htmlFile));
// copy files to move
for (let i = 0; i < filesToMove.length; i++)
await copy(filesToMove[i], path.join(targetDirectory, path.basename(filesToMove[i])));
// read html file
let html = await fs.readFile(path.join(targetDirectory, htmlFile), "utf8");
// remove base paths from files to move
for (let i = 0; i < filesToMove.length; i++) {
const basePath = `${filesToMove[i].split("/").slice(0, -1).join("/")}/`;
html = html.replace(basePath, "");
}
// remove basePath from source directory
const basePath = `${path.basename(sourceDirectory)}/`;
const re = new RegExp(basePath, "g");
html = html.replace(re, "");
// write html files
await fs.writeFile(`${targetDirectory}/${htmlFile}`, html);
console.log("done!!");
};
build();

View file

@ -0,0 +1,25 @@
/* global app, $on */
(function () {
"use strict";
/**
* Sets up a brand new Todo list.
*
* @param {string} name The name of your new to do list.
*/
function Todo(name) {
this.storage = new app.Store(name);
this.model = new app.Model(this.storage);
this.template = new app.Template();
this.view = new app.View(this.template);
this.controller = new app.Controller(this.model, this.view);
}
var todo = new Todo("javascript-es5");
function setView() {
todo.controller.setView(document.location.hash);
}
$on(window, "load", setView);
$on(window, "hashchange", setView);
})();

View file

@ -0,0 +1,266 @@
(function (window) {
"use strict";
/**
* Takes a model and view and acts as the controller between them
*
* @constructor
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var self = this;
self.model = model;
self.view = view;
self.view.bind("newTodo", function (title) {
self.addItem(title);
});
self.view.bind("itemEdit", function (item) {
self.editItem(item.id);
});
self.view.bind("itemEditDone", function (item) {
self.editItemSave(item.id, item.title);
});
self.view.bind("itemEditCancel", function (item) {
self.editItemCancel(item.id);
});
self.view.bind("itemRemove", function (item) {
self.removeItem(item.id);
});
self.view.bind("itemToggle", function (item) {
self.toggleComplete(item.id, item.completed);
});
self.view.bind("removeCompleted", function () {
self.removeCompletedItems();
});
self.view.bind("toggleAll", function (status) {
self.toggleAll(status.completed);
});
}
/**
* Loads and initialises the view
*
* @param {string} '' | 'active' | 'completed'
*/
Controller.prototype.setView = function (locationHash) {
var route = locationHash.split("/")[1];
var page = route || "";
this._updateFilterState(page);
};
/**
* An event to fire on load. Will get all items and display them in the
* todo-list
*/
Controller.prototype.showAll = function () {
var self = this;
self.model.read(function (data) {
self.view.render("showEntries", data);
});
};
/**
* Renders all active tasks
*/
Controller.prototype.showActive = function () {
var self = this;
self.model.read({ completed: false }, function (data) {
self.view.render("showEntries", data);
});
};
/**
* Renders all completed tasks
*/
Controller.prototype.showCompleted = function () {
var self = this;
self.model.read({ completed: true }, function (data) {
self.view.render("showEntries", data);
});
};
/**
* An event to fire whenever you want to add an item. Simply pass in the event
* object and it'll handle the DOM insertion and saving of the new item.
*/
Controller.prototype.addItem = function (title) {
var self = this;
if (title.trim() === "")
return;
self.model.create(title, function () {
self.view.render("clearNewTodo");
self._filter(true);
});
};
/*
* Triggers the item editing mode.
*/
Controller.prototype.editItem = function (id) {
var self = this;
self.model.read(id, function (data) {
self.view.render("editItem", { id: id, title: data[0].title });
});
};
/*
* Finishes the item editing mode successfully.
*/
Controller.prototype.editItemSave = function (id, title) {
var self = this;
title = title.trim();
if (title.length !== 0) {
self.model.update(id, { title: title }, function () {
self.view.render("editItemDone", { id: id, title: title });
});
} else {
self.removeItem(id);
}
};
/*
* Cancels the item editing mode.
*/
Controller.prototype.editItemCancel = function (id) {
var self = this;
self.model.read(id, function (data) {
self.view.render("editItemDone", { id: id, title: data[0].title });
});
};
/**
* By giving it an ID it'll find the DOM element matching that ID,
* remove it from the DOM and also remove it from storage.
*
* @param {number} id The ID of the item to remove from the DOM and
* storage
*/
Controller.prototype.removeItem = function (id) {
var self = this;
self.model.remove(id, function () {
self.view.render("removeItem", id);
});
self._filter();
};
/**
* Will remove all completed items from the DOM and storage.
*/
Controller.prototype.removeCompletedItems = function () {
var self = this;
self.model.read({ completed: true }, function (data) {
data.forEach(function (item) {
self.removeItem(item.id);
});
});
self._filter();
};
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
*
* @param {number} id The ID of the element to complete or uncomplete
* @param {object} checkbox The checkbox to check the state of complete
* or not
* @param {boolean|undefined} silent Prevent re-filtering the todo items
*/
Controller.prototype.toggleComplete = function (id, completed, silent) {
var self = this;
self.model.update(id, { completed: completed }, function () {
self.view.render("elementComplete", {
id: id,
completed: completed,
});
});
if (!silent)
self._filter();
};
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
Controller.prototype.toggleAll = function (completed) {
var self = this;
self.model.read({ completed: !completed }, function (data) {
data.forEach(function (item) {
self.toggleComplete(item.id, completed, true);
});
});
self._filter();
};
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
Controller.prototype._updateCount = function () {
var self = this;
self.model.getCount(function (todos) {
self.view.render("updateElementCount", todos.active);
self.view.render("clearCompletedButton", {
completed: todos.completed,
visible: todos.completed > 0,
});
self.view.render("toggleAll", { checked: todos.completed === todos.total });
self.view.render("contentBlockVisibility", { visible: todos.total > 0 });
});
};
/**
* Re-filters the todo items, based on the active route.
* @param {boolean|undefined} force forces a re-painting of todo items.
*/
Controller.prototype._filter = function (force) {
var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1);
// Update the elements on the page, which change with each completed todo
this._updateCount();
// If the last active route isn't "All", or we're switching routes, we
// re-create the todo item elements, calling:
// this.show[All|Active|Completed]();
if (force || this._lastActiveRoute !== "All" || this._lastActiveRoute !== activeRoute)
this[`show${activeRoute}`]();
this._lastActiveRoute = activeRoute;
};
/**
* Simply updates the filter nav's selected states
*/
Controller.prototype._updateFilterState = function (currentPage) {
// Store a reference to the active route, allowing us to re-filter todo
// items as they are marked complete or incomplete.
this._activeRoute = currentPage;
if (currentPage === "")
this._activeRoute = "All";
this._filter();
this.view.render("setFilter", currentPage);
};
// Export to window
window.app = window.app || {};
window.app.Controller = Controller;
})(window);

View file

@ -0,0 +1,51 @@
/* global NodeList */
(function (window) {
"use strict";
// Get element(s) by CSS selector:
window.qs = function (selector, scope) {
return (scope || document).querySelector(selector);
};
window.qsa = function (selector, scope) {
return (scope || document).querySelectorAll(selector);
};
// addEventListener wrapper:
window.$on = function (target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
};
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
window.$delegate = function (target, selector, type, handler) {
function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = window.qsa(selector, target);
var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
if (hasMatch)
handler.call(targetElement, event);
}
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === "blur" || type === "focus";
window.$on(target, type, dispatchEvent, useCapture);
};
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div');
window.$parent = function (element, tagName) {
if (!element.parentNode)
return undefined;
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase())
return element.parentNode;
return window.$parent(element.parentNode, tagName);
};
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
})(window);

View file

@ -0,0 +1,119 @@
(function (window) {
"use strict";
/**
* Creates a new Model instance and hooks up the storage.
*
* @constructor
* @param {object} storage A reference to the client side storage class
*/
function Model(storage) {
this.storage = storage;
}
/**
* Creates a new todo model
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
Model.prototype.create = function (title, callback) {
title = title || "";
callback = callback || function () {};
var newItem = {
title: title.trim(),
completed: false,
};
this.storage.save(newItem, callback);
};
/**
* Finds and returns a model in storage. If no query is given it'll simply
* return everything. If you pass in a string or number it'll look that up as
* the ID of the model to find. Lastly, you can pass it an object to match
* against.
*
* @param {string|number|object} [query] A query to match models against
* @param {function} [callback] The callback to fire after the model is found
*
* @example
* model.read(1, func); // Will find the model with an ID of 1
* model.read('1'); // Same as above
* //Below will find a model with foo equalling bar and hello equalling world.
* model.read({ foo: 'bar', hello: 'world' });
*/
Model.prototype.read = function (query, callback) {
var queryType = typeof query;
callback = callback || function () {};
if (queryType === "function") {
callback = query;
return this.storage.findAll(callback);
} else if (queryType === "string" || queryType === "number") {
query = parseInt(query, 10);
return this.storage.find({ id: query }, callback);
} else {
return this.storage.find(query, callback);
}
};
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is complete.
*
* @param {number} id The id of the model to update
* @param {object} data The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
Model.prototype.update = function (id, data, callback) {
this.storage.save(data, callback, id);
};
/**
* Removes a model from storage
*
* @param {number} id The ID of the model to remove
* @param {function} callback The callback to fire when the removal is complete.
*/
Model.prototype.remove = function (id, callback) {
this.storage.remove(id, callback);
};
/**
* WARNING: Will remove ALL data from storage.
*
* @param {function} callback The callback to fire when the storage is wiped.
*/
Model.prototype.removeAll = function (callback) {
this.storage.drop(callback);
};
/**
* Returns a count of all todos
*/
Model.prototype.getCount = function (callback) {
var todos = {
active: 0,
completed: 0,
total: 0,
};
this.storage.findAll(function (data) {
data.forEach(function (todo) {
if (todo.completed)
todos.completed++;
else
todos.active++;
todos.total++;
});
callback(todos);
});
};
// Export to window
window.app = window.app || {};
window.app.Model = Model;
})(window);

View file

@ -0,0 +1,177 @@
/* jshint eqeqeq:false */
(function (window) {
"use strict";
var MemoryStorage = {
_data: {},
_name: "",
init: function (name, data = {}) {
this._name = name;
if (!this.getData())
this.setData(data);
},
getData: function () {
const data = this._data[this._name];
if (!data)
return null;
return JSON.parse(data);
},
setData: function (value) {
this._data[this._name] = JSON.stringify(value);
},
};
var LocalStorage = {
_name: "",
init: function (name, data = {}) {
this._name = name;
if (!this.getData())
this.setData(data);
},
getData: function () {
const data = window.localStorage.getItem(this._name);
if (!data)
return null;
return JSON.parse(data);
},
setData: function (value) {
window.localStorage.setItem(this._name, JSON.stringify(value));
},
};
var ID = 1;
/**
* Creates a new client side storage object and will create an empty
* collection if no collection already exists.
*
* @param {string} name The name of our DB we want to use
* @param {function} callback Our fake DB uses callbacks because in
* real life you probably would be making AJAX calls
*/
function Store(name, callback, type = "disk") {
callback = callback || function () {};
this._dbName = name;
this.storage = type === "memory" ? MemoryStorage : LocalStorage;
this.storage.init(name, []);
callback.call(this, this.storage.getData());
}
/**
* Finds items based on a query given as a JS object
*
* @param {object} query The query to match against (i.e. {foo: 'bar'})
* @param {function} callback The callback to fire when the query has
* completed running
*
* @example
* db.find({foo: 'bar', hello: 'world'}, function (data) {
* // data will return any items that have foo: bar and
* // hello: world in their properties
* });
*/
Store.prototype.find = function (query, callback) {
if (!callback)
return;
var todos = this.storage.getData();
callback.call(
this,
todos.filter(function (todo) {
for (var q in query) {
if (query[q] !== todo[q])
return false;
}
return true;
})
);
};
/**
* Will retrieve all data from the collection
*
* @param {function} callback The callback to fire upon retrieving data
*/
Store.prototype.findAll = function (callback) {
callback = callback || function () {};
callback.call(this, this.storage.getData());
};
/**
* Will save the given data to the DB. If no item exists it will create a new
* item, otherwise it'll simply update an existing item's properties
*
* @param {object} updateData The data to save back into the DB
* @param {function} callback The callback to fire after saving
* @param {number} id An optional param to enter an ID of an item to update
*/
Store.prototype.save = function (updateData, callback, id) {
var todos = this.storage.getData();
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
for (var key in updateData)
todos[i][key] = updateData[key];
break;
}
}
this.storage.setData(todos);
callback.call(this, todos);
} else {
// Generate an ID
updateData.id = ID++;
todos.push(updateData);
this.storage.setData(todos);
callback.call(this, [updateData]);
}
};
/**
* Will remove an item from the Store based on its ID
*
* @param {number} id The ID of the item you want to remove
* @param {function} callback The callback to fire after saving
*/
Store.prototype.remove = function (id, callback) {
var todos = this.storage.getData();
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todos.splice(i, 1);
break;
}
}
this.storage.setData(todos);
callback.call(this, todos);
};
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
Store.prototype.drop = function (callback) {
var data = [];
this.storage.setData(data);
callback.call(this, data);
};
// Export to window
window.app = window.app || {};
window.app.Store = Store;
})(window);

View file

@ -0,0 +1,112 @@
/* jshint laxbreak:true */
(function (window) {
"use strict";
var htmlEscapes = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;",
};
var escapeHtmlChar = function (chr) {
return htmlEscapes[chr];
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function (string) {
return string && reHasUnescapedHtml.test(string) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string;
};
/**
* Sets up defaults for all the Template methods such as a default template
*
* @constructor
*/
function Template() {
this.defaultTemplate
= '<li data-id="{{id}}" class="{{completed}}" data-priority="{{priority}}">'
+ '<div class="view">'
+ '<input class="toggle" type="checkbox" {{checked}}>'
+ "<label>{{title}}</label>"
+ '<button class="destroy"></button>'
+ "</div>"
+ "</li>";
}
/**
* Creates an <li> HTML string and returns it for placement in your app.
*
* NOTE: In real life you should be using a templating engine such as Mustache
* or Handlebars, however, this is a vanilla JS example.
*
* @param {object} data The object containing keys you want to find in the
* template to replace.
* @returns {string} HTML String of an <li> element
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* });
*/
Template.prototype.show = function (data) {
var i, l;
var view = "";
for (i = 0, l = data.length; i < l; i++) {
var template = this.defaultTemplate;
var completed = "";
var checked = "";
if (data[i].completed) {
completed = "completed";
checked = "checked";
}
template = template.replace("{{id}}", data[i].id);
template = template.replace("{{title}}", escape(data[i].title));
template = template.replace("{{completed}}", completed);
template = template.replace("{{checked}}", checked);
template = template.replace("{{priority}}", 4 - (i % 5));
view = view + template;
}
return view;
};
/**
* Displays a counter of how many to dos are left to complete
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
Template.prototype.itemCounter = function (activeTodos) {
var plural = activeTodos === 1 ? "" : "s";
return `<strong>${activeTodos}</strong> item${plural} left`;
};
/**
* Updates the text within the "Clear completed" button
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
*/
Template.prototype.clearCompletedButton = function (completedTodos) {
if (completedTodos > 0)
return "Clear completed";
else
return "";
};
// Export to window
window.app = window.app || {};
window.app.Template = Template;
})(window);

View file

@ -0,0 +1,215 @@
/* global qs, qsa, $on, $parent, $delegate */
(function (window) {
"use strict";
/**
* View that abstracts away the browser's DOM completely.
* It has two simple entry points:
*
* - bind(eventName, handler)
* Takes a todo application event and registers the handler
* - render(command, parameterObject)
* Renders the given command with the options
*/
function View(template) {
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = qs(".todo-list");
this.$todoItemCounter = qs(".todo-count");
this.$clearCompleted = qs(".clear-completed");
this.$main = qs(".main");
this.$footer = qs(".footer");
this.$toggleAllInput = qs(".toggle-all");
this.$toggleAll = qs(".toggle-all-label");
this.$newTodo = qs(".new-todo");
}
View.prototype._removeItem = function (id) {
var elem = qs(`[data-id="${id}"]`);
if (elem)
this.$todoList.removeChild(elem);
};
View.prototype._clearCompletedButton = function (completedCount, visible) {
this.$clearCompleted.textContent = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? "block" : "none";
};
View.prototype._setFilter = function (currentPage) {
qs(".filters .selected").className = "";
qs(`.filters [href="#/${currentPage}"]`).className = "selected";
};
View.prototype._elementComplete = function (id, completed) {
var listItem = qs(`[data-id="${id}"]`);
if (!listItem)
return;
listItem.className = completed ? "completed" : "";
// In case it was toggled from an event and not by clicking the checkbox
qs("input", listItem).checked = completed;
};
View.prototype._editItem = function (id, title) {
var listItem = qs(`[data-id="${id}"]`);
if (!listItem)
return;
listItem.className = `${listItem.className} editing`;
var input = document.createElement("input");
input.className = "edit";
listItem.appendChild(input);
input.focus();
input.value = title;
};
View.prototype._editItemDone = function (id, title) {
var listItem = qs(`[data-id="${id}"]`);
if (!listItem)
return;
var input = qs("input.edit", listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace("editing", "");
qsa("label", listItem).forEach(function (label) {
label.textContent = title;
});
};
View.prototype._replaceContentWithHtml = function (element, html) {
const parsedDocument = new DOMParser().parseFromString(html, "text/html");
element.replaceChildren(...parsedDocument.body.childNodes);
};
View.prototype.render = function (viewCmd, parameter) {
var self = this;
var viewCommands = {
showEntries: function () {
self._replaceContentWithHtml(self.$todoList, self.template.show(parameter));
},
removeItem: function () {
self._removeItem(parameter);
},
updateElementCount: function () {
self._replaceContentWithHtml(self.$todoItemCounter, self.template.itemCounter(parameter));
},
clearCompletedButton: function () {
self._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function () {
self.$main.style.display = self.$footer.style.display = parameter.visible ? "block" : "none";
},
toggleAll: function () {
self.$toggleAll.checked = parameter.checked;
},
setFilter: function () {
self._setFilter(parameter);
},
clearNewTodo: function () {
self.$newTodo.value = "";
},
elementComplete: function () {
self._elementComplete(parameter.id, parameter.completed);
},
editItem: function () {
self._editItem(parameter.id, parameter.title);
},
editItemDone: function () {
self._editItemDone(parameter.id, parameter.title);
},
};
viewCommands[viewCmd]();
};
View.prototype._itemId = function (element) {
var li = $parent(element, "li");
return parseInt(li.dataset.id, 10);
};
View.prototype._bindItemEditDone = function (handler) {
var self = this;
$delegate(self.$todoList, "li .edit", "blur", function () {
if (!this.dataset.iscanceled) {
handler({
id: self._itemId(this),
title: this.value,
});
}
});
$delegate(self.$todoList, "li .edit", "keypress", function (event) {
if (event.keyCode === self.ENTER_KEY) {
// Remove the cursor from the input when you hit enter just like if it
// were a real form
this.blur();
}
});
};
View.prototype._bindItemEditCancel = function (handler) {
var self = this;
$delegate(self.$todoList, "li .edit", "keyup", function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
this.dataset.iscanceled = true;
this.blur();
handler({ id: self._itemId(this) });
}
});
};
View.prototype.bind = function (event, handler) {
var self = this;
if (event === "newTodo") {
$on(self.$newTodo, "change", function () {
handler(self.$newTodo.value);
});
} else if (event === "removeCompleted") {
$on(self.$clearCompleted, "click", function () {
handler();
});
} else if (event === "toggleAll") {
$on(self.$toggleAll, "click", function () {
self.$toggleAllInput.click();
handler({ completed: self.$toggleAllInput.checked });
});
} else if (event === "itemEdit") {
$delegate(self.$todoList, "li label", "dblclick", function () {
handler({ id: self._itemId(this) });
});
} else if (event === "itemRemove") {
$delegate(self.$todoList, ".destroy", "click", function () {
handler({ id: self._itemId(this) });
});
} else if (event === "itemToggle") {
$delegate(self.$todoList, ".toggle", "click", function () {
handler({
id: self._itemId(this),
completed: this.checked,
});
});
} else if (event === "itemEditDone") {
self._bindItemEditDone(handler);
} else if (event === "itemEditCancel") {
self._bindItemEditCancel(handler);
}
};
// Export to window
window.app = window.app || {};
window.app.View = View;
})(window);

128
index.html Normal file
View file

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html data-visible-section="home">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer 3.0</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
<script src="resources/main.mjs" type="module"></script>
</head>
<body>
<main>
<section id="home">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version next">next</div>
</a>
<div class="content">
<p>Speedometer is a browser benchmark that measures the responsiveness of Web applications. It uses demo web applications to simulate user actions such as adding to-do items.</p>
<p id="screen-size-warning">
<strong>
Your browser window is too small. For most accurate results, please make the view port size at least <span id="min-screen-width">850px</span> by <span id="min-screen-height">650px</span>.<br />
It's currently <span id="screen-size"></span>.
</strong>
</p>
</div>
<div class="buttons">
<div class="button-row">
<button class="start-tests-button">Start Test</button>
</div>
<div class="button-row">
<a href="about.html">About Speedometer</a>
<a href="instructions.html">Test Instructions</a>
</div>
</div>
</section>
<section id="running">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div id="testContainer"></div>
<div id="leakedIframes"></div>
<div id="progress">
<progress aria-label="Progress" id="progress-completed"></progress>
</div>
<div id="info">
<div id="info-label"></div>
<div id="info-progress"></div>
</div>
</section>
<section id="summary" data-title="Results Summary" class="valid">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<h1>Score</h1>
<div class="gauge">
<div class="window"><div class="needle"></div></div>
</div>
<hr />
<div id="result-number"></div>
<div id="confidence-number"></div>
<div id="invalid-score-text">
One or more subtests produced no duration.<br />
Please check your <a href="./instructions.html" target="_blank">browser settings</a> and re-run the benchmark.<br />
</div>
<div class="buttons">
<div class="button-row">
<a class="button" href="#details" id="show-details" title="Show detailed results data.">Details</a>
</div>
</div>
</section>
<section id="details" data-title="Results Details">
<a href="#home" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="section-grid">
<h1 class="section-header">Detailed Results</h1>
<div class="section-content all-metric-results">
<div class="non-standard-params">
<h2>Non-standard Parameters</h2>
<p>
Speedometer ran with non-standard parameters.<br />
The results are likely not comparable to default runs.
</p>
<table id="non-standard-params-table">
<thead>
<tr>
<th>Param</th>
<th>Value</th>
<th>Default</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="aggregated-metric-result">
<h2>Aggregate Metric</h2>
<div id="geomean-chart"></div>
<h2>Test Metrics Overview</h2>
<div id="tests-chart"></div>
</div>
<br />
<h2>Test Metrics Details</h2>
<div id="metrics-results"></div>
</div>
<div class="buttons section-footer">
<div class="button-row">
<a class="button" href="#summary" title="Go back to the simplified summary view.">Summary</a>
</div>
<div class="button-row export-buttons">
<button id="copy-full-json" title="Copy full result metrics as json string.">Copy JSON</button>
<a class="button" id="download-full-json" title="Download full result metrics as json string.">Download JSON</a>
<a class="button" id="download-classic-json" title="Download backwards-compatible result metrics as json string.">Download Classic JSON</a>
<a class="button" id="download-csv" title="Download all result metrics as CSV string.">Download CSV</a>
<button id="copy-csv" title="Copy all result metrics as CSV string.">Copy CSV</button>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

42
instructions.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=850" />
<title>Speedometer 3.0 Test Instructions</title>
<link rel="stylesheet" href="resources/main.css" />
<link rel="icon" href="resources/favicon.png" />
</head>
<body>
<main>
<section id="instructions" class="visible" data-title="Test Instructions">
<a href="./" class="logo">
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
<div class="version">3.0</div>
</a>
<div class="section-grid">
<h1 class="section-header">Speedometer 3 General Instructions</h1>
<div class="section-content">
<p>To get reasonably accurate results your device and browser need to be focused on the benchmark. Here are some basic instructions to allow that:</p>
<ul class="compact">
<li>Ensure you are running the latest stable browser version.</li>
<li>Prefer using a separate clean browser profile — extensions and non-default browser settings can have a large impact on the score.</li>
<li>Don't run any programs in the background.</li>
<li>Restart the browser and close all other tabs and windows.</li>
<li>Keep the Speedometer page focused and do not interact with the device while running the benchmark.</li>
<li>Keep your battery-powered device plugged into a charger.</li>
<li>Some devices might need time to cool down between consecutive runs.</li>
</ul>
</div>
<div class="buttons section-footer">
<div class="button-row">
<a class="button" href="./" title="Show main section.">Home</a>
<a class="button" href="about.html" title="Show about section.">About</a>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

7
netlify.toml Normal file
View file

@ -0,0 +1,7 @@
[[headers]]
for = "/*"
[headers.values]
cross-origin-embedder-policy = "require-corp"
cross-origin-opener-policy = "same-origin"
X-Frame-Options = "SAMEORIGIN"
x-content-type-options = "nosniff"

11542
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

56
package.json Normal file
View file

@ -0,0 +1,56 @@
{
"name": "speedometer",
"version": "3.0.0-alpha",
"description": "An open source repository for the Speedometer benchmark.",
"engines": {
"node": ">=22.0.0",
"npm": ">=8.19.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WebKit/Speedometer.git"
},
"bugs": {
"url": "https://github.com/WebKit/Speedometer/issues"
},
"license": "SEE LICENSE IN LICENSE",
"scripts": {
"dev": "http-server ./ -c-1 --cors",
"server": "http-server ./ --cors",
"lint:check": "eslint **/*.{js,mjs,jsx,ts,tsx}",
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
"pretty:check": "prettier --check ./",
"pretty:fix": "prettier --write ./",
"format": "npm run pretty:fix && npm run lint:fix",
"test:chrome": "node tests/run-unittests.mjs --browser chrome",
"test:firefox": "node tests/run-unittests.mjs --browser firefox",
"test:safari": "node tests/run-unittests.mjs --browser safari",
"test:edge": "node tests/run-unittests.mjs --browser edge",
"test-e2e:chrome": "node tests/run-end2end.mjs --browser chrome",
"test-e2e:firefox": "node tests/run-end2end.mjs --browser firefox",
"test-e2e:safari": "node tests/run-end2end.mjs --browser safari",
"test-e2e:edge": "node tests/run-end2end.mjs --browser edge"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@nuxt/eslint-config": "^0.1.1",
"@next/eslint-plugin-next": "^13.4.5",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^6.1.3",
"eslint": "^8.38.0",
"eslint-plugin-ember": "^11.4.8",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-vue": "^9.10.0",
"expect.js": "^0.3.1",
"http-server": "^14.1.1",
"mocha": "^10.2.0",
"prettier": "^2.8.3",
"selenium-webdriver": "^4.27.0",
"sinon": "^17.0.1",
"typescript": "^5.0.4"
}
}

View file

@ -0,0 +1,518 @@
import { Metric } from "./metric.mjs";
import { params } from "./shared/params.mjs";
import { SUITE_RUNNER_LOOKUP } from "./suite-runner.mjs";
const performance = globalThis.performance;
export class BenchmarkTestStep {
constructor(testName, testFunction) {
this.name = testName;
this.run = testFunction;
}
}
function getParent(lookupStartNode, path) {
const parent = path.reduce((root, selector) => {
const node = root.querySelector(selector);
return node.shadowRoot ?? node;
}, lookupStartNode);
return parent;
}
class Page {
constructor(frame) {
this._frame = frame;
}
getLocalStorage() {
return this._frame.contentWindow.localStorage;
}
layout() {
const body = this._frame.contentDocument.body.getBoundingClientRect();
this.layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
}
async waitForElement(selector) {
return new Promise((resolve) => {
const resolveIfReady = () => {
const element = this.querySelector(selector);
let callback = resolveIfReady;
if (element)
callback = () => resolve(element);
window.requestAnimationFrame(callback);
};
resolveIfReady();
});
}
/**
* Returns the first element within the document that matches the specified selector, or group of selectors.
* If no matches are found, null is returned.
*
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
*
* @example
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
* // return PageElement(<todo-item>)
* querySelector("todo-item", ["todo-app", "todo-list"]);
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns PageElement | null
*/
querySelector(selector, path = []) {
const lookupStartNode = this._frame.contentDocument;
const element = getParent(lookupStartNode, path).querySelector(selector);
if (element === null)
return null;
return this._wrapElement(element);
}
/**
* Returns all elements within the document that matches the specified selector, or group of selectors.
* If no matches are found, null is returned.
*
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
*
* @example
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
* // return [PageElement(<todo-item>), PageElement(<todo-item>)]
* querySelectorAll("todo-item", ["todo-app", "todo-list"]);
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns array
*/
querySelectorAll(selector, path = []) {
const lookupStartNode = this._frame.contentDocument;
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
for (let i = 0; i < elements.length; i++)
elements[i] = this._wrapElement(elements[i]);
return elements;
}
getElementById(id) {
const element = this._frame.contentDocument.getElementById(id);
if (element === null)
return null;
return this._wrapElement(element);
}
call(functionName) {
this._frame.contentWindow[functionName]();
return null;
}
callAsync(functionName) {
setTimeout(() => {
this._frame.contentWindow[functionName]();
}, 0);
}
callToGetElement(functionName) {
return this._wrapElement(this._frame.contentWindow[functionName]());
}
_wrapElement(element) {
return new PageElement(element);
}
}
const NATIVE_OPTIONS = {
bubbles: true,
cancellable: true,
};
class PageElement {
#node;
constructor(node) {
this.#node = node;
}
setValue(value) {
this.#node.value = value;
}
click() {
this.#node.click();
}
focus() {
this.#node.focus();
}
getElementByMethod(name) {
return new PageElement(this.#node[name]());
}
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
if (eventName === "submit")
// FIXME FireFox doesn't like `new Event('submit')
this._dispatchSubmitEvent();
else
this.#node.dispatchEvent(new eventType(eventName, options));
}
_dispatchSubmitEvent() {
const submitEvent = document.createEvent("Event");
submitEvent.initEvent("submit", true, true);
this.#node.dispatchEvent(submitEvent);
}
enter(type, options = undefined) {
const ENTER_KEY_CODE = 13;
return this.dispatchKeyEvent(type, ENTER_KEY_CODE, "Enter", options);
}
dispatchKeyEvent(type, keyCode, key, options) {
let eventOptions = { bubbles: true, cancelable: true, keyCode, which: keyCode, key };
if (options !== undefined)
eventOptions = Object.assign(eventOptions, options);
const event = new KeyboardEvent(type, eventOptions);
this.#node.dispatchEvent(event);
}
dispatchMouseEvent(type, offsetX, offsetY, options) {
const boundingRect = this.#node.getBoundingClientRect();
const clientX = offsetX + boundingRect.x;
const clientY = offsetY + boundingRect.y;
const contentWindow = this.#node.ownerDocument.defaultView;
const screenX = clientX + contentWindow.screenX;
const screenY = clientY + contentWindow.screenY;
let eventOptions = { bubbles: true, cancelable: true, clientX, clientY, screenX, screenY };
if (options !== undefined)
eventOptions = Object.assign(eventOptions, options);
const event = new contentWindow.MouseEvent(type, eventOptions);
this.#node.dispatchEvent(event);
}
/**
* Returns the first element found in a node of a PageElement that matches the specified selector, or group of selectors. If a shadow DOM is present in the node, the shadow DOM is used to query.
* If no matches are found, null is returned.
*
* @param {string} selector A string containing one or more selectors to match.
* @param {string[]} [path] An array containing a path to the parent element.
* @returns PageElement | null
*/
querySelectorInShadowRoot(selector, path = []) {
const lookupStartNode = this.#node.shadowRoot ?? this.#node;
const element = getParent(lookupStartNode, path).querySelector(selector);
if (element === null)
return null;
return new PageElement(element);
}
querySelector(selector) {
const element = this.#node.querySelector(selector);
if (element === null)
return null;
return new PageElement(element);
}
}
function geomeanToScore(geomean) {
return 1000 / geomean;
}
// The WarmupSuite is used to make sure all runner helper functions and
// classes are compiled, to avoid unnecessary pauses due to delayed
// compilation of runner methods in the middle of the measuring cycle.
export const WarmupSuite = {
name: "Warmup",
url: "warmup/index.html",
async prepare(page) {
await page.waitForElement("#testItem");
},
tests: [
// Make sure to run ever page.method once at least
new BenchmarkTestStep("WarmingUpPageMethods", (page) => {
let results = [];
results.push(page.querySelector(".testItem"));
results.push(page.querySelectorAll(".item"));
results.push(page.getElementById("testItem"));
}),
new BenchmarkTestStep("WarmingUpPageElementMethods", (page) => {
const item = page.getElementById("testItem");
item.setValue("value");
item.click();
item.focus();
item.dispatchEvent("change");
item.enter("keypress");
item.dispatchEvent("input");
item.enter("keyup");
}),
new BenchmarkTestStep("WarmingUpPageElementMouseMethods", (page) => {
const item = page.getElementById("testItem");
const mouseEventOptions = { clientX: 100, clientY: 100, bubbles: true, cancelable: true };
const wheelEventOptions = {
clientX: 200,
clientY: 200,
deltaMode: 0,
delta: -10,
deltaY: -10,
bubbles: true,
cancelable: true,
};
item.dispatchEvent("mousedown", mouseEventOptions, MouseEvent);
item.dispatchEvent("mousemove", mouseEventOptions, MouseEvent);
item.dispatchEvent("mouseup", mouseEventOptions, MouseEvent);
item.dispatchEvent("wheel", wheelEventOptions, WheelEvent);
}),
],
};
// https://stackoverflow.com/a/47593316
function seededHashRandomNumberGenerator(a) {
return function () {
var t = a += 0x6d2b79f5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return (t ^ (t >>> 14)) >>> 0;
};
}
class WakeLock {
#wakeLockSentinel = undefined;
async request() {
if (!navigator.wakeLock)
return;
try {
this.#wakeLockSentinel = await navigator.wakeLock.request("screen");
} catch (err) {
console.error(`${err.name}, ${err.message}`);
}
}
async release() {
if (!this.#wakeLockSentinel)
return;
try {
await this.#wakeLockSentinel.release();
} catch (err) {
console.error(`${err.name}, ${err.message}`);
} finally {
this.#wakeLockSentinel = undefined;
}
}
}
export class BenchmarkRunner {
constructor(suites, client) {
this._suites = suites;
if (params.useWarmupSuite)
this._suites = [WarmupSuite, ...suites];
this._client = client;
this._page = null;
this._metrics = null;
this._iterationCount = params.iterationCount;
if (params.shuffleSeed !== "off")
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
this._wakeLock = new WakeLock();
}
async runMultipleIterations(iterationCount) {
this._iterationCount = iterationCount;
if (this._client?.willStartFirstIteration)
await this._client.willStartFirstIteration(iterationCount);
try {
await this._runMultipleIterations();
} catch (error) {
console.error(error);
if (this._client?.handleError) {
await this._client.handleError(error);
return;
}
}
if (this._client?.didFinishLastIteration)
await this._client.didFinishLastIteration(this._metrics);
}
async _runMultipleIterations() {
const iterationStartLabel = "iteration-start";
const iterationEndLabel = "iteration-end";
for (let i = 0; i < this._iterationCount; i++) {
performance.mark(iterationStartLabel);
await this.runAllSuites();
performance.mark(iterationEndLabel);
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
}
}
_removeFrame() {
if (this._frame) {
if (params.leakyIframes)
document.getElementById("leakedIframes").append(this._frame);
else
this._frame.parentNode.removeChild(this._frame);
this._frame = null;
}
}
async _appendFrame() {
const frame = document.createElement("iframe");
const style = frame.style;
style.width = `${params.viewport.width}px`;
style.height = `${params.viewport.height}px`;
style.border = "0px none";
style.position = "absolute";
frame.setAttribute("scrolling", "no");
frame.className = "test-runner";
style.left = "50%";
style.top = "50%";
style.transform = "translate(-50%, -50%)";
if (this._client?.willAddTestFrame)
await this._client.willAddTestFrame(frame);
document.body.insertBefore(frame, document.body.firstChild);
this._frame = frame;
return frame;
}
async _prepareAllSuites() {
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
await this._wakeLock.request();
const prepareStartLabel = "runner-prepare-start";
const prepareEndLabel = "runner-prepare-end";
performance.mark(prepareStartLabel);
let suites = [...this._suites];
if (this._suiteOrderRandomNumberGenerator)
this._shuffleSuites(suites);
performance.mark(prepareEndLabel);
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
return suites;
}
_shuffleSuites(suites) {
// We just do a simple Fisher-Yates shuffle based on the repeated hash of the
// seed. This is not a high quality RNG, but it's plenty good enough.
for (let i = 0; i < suites.length - 1; i++) {
const j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
const tmp = suites[i];
suites[i] = suites[j];
suites[j] = tmp;
}
}
async runAllSuites() {
const suites = await this._prepareAllSuites();
try {
for (const suite of suites) {
if (suite.disabled)
continue;
try {
await this._appendFrame();
this._page = new Page(this._frame);
await this.runSuite(suite);
} finally {
this._removeFrame();
}
}
} finally {
await this._finishRunAllSuites();
}
}
async _finishRunAllSuites() {
const finalizeStartLabel = "runner-finalize-start";
const finalizeEndLabel = "runner-finalize-end";
performance.mark(finalizeStartLabel);
await this._finalize();
performance.mark(finalizeEndLabel);
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
await this._wakeLock.release();
}
async runSuite(suite) {
// FIXME: Encapsulate more state in the SuiteRunner.
// FIXME: Return and use measured values from SuiteRunner.
const type = suite.type ?? ((params.useAsyncSteps && "async") || "default");
const suiteRunnerClass = SUITE_RUNNER_LOOKUP[type];
const suiteRunner = new suiteRunnerClass(this._frame, this._page, params, suite, this._client, this._measuredValues);
await suiteRunner.run();
}
async _finalize() {
this._appendIterationMetrics();
if (this._client?.didRunSuites) {
let product = 1;
const values = [];
for (const suiteName in this._measuredValues.tests) {
const suiteTotal = this._measuredValues.tests[suiteName].total;
product *= suiteTotal;
values.push(suiteTotal);
}
values.sort((a, b) => a - b); // Avoid the loss of significance for the sum.
const total = values.reduce((a, b) => a + b);
const geomean = Math.pow(product, 1 / values.length);
this._measuredValues.total = total;
this._measuredValues.mean = total / values.length;
this._measuredValues.geomean = geomean;
this._measuredValues.score = geomeanToScore(geomean);
await this._client.didRunSuites(this._measuredValues);
}
}
_appendIterationMetrics() {
const getMetric = (name, unit = "ms") => this._metrics[name] || (this._metrics[name] = new Metric(name, unit));
const iterationTotalMetric = (i) => {
if (i >= params.iterationCount)
throw new Error(`Requested iteration=${i} does not exist.`);
return getMetric(`Iteration-${i}-Total`);
};
const collectSubMetrics = (prefix, items, parent) => {
for (let name in items) {
const results = items[name];
const metric = getMetric(prefix + name);
metric.add(results.total ?? results);
if (metric.parent !== parent)
parent.addChild(metric);
if (results.tests)
collectSubMetrics(`${metric.name}${Metric.separator}`, results.tests, metric);
}
};
const initializeMetrics = this._metrics === null;
if (initializeMetrics)
this._metrics = { __proto__: null };
const iterationResults = this._measuredValues.tests;
collectSubMetrics("", iterationResults);
if (initializeMetrics) {
// Prepare all iteration metrics so they are listed at the end of
// of the _metrics object, before "Total" and "Score".
for (let i = 0; i < this._iterationCount; i++)
iterationTotalMetric(i).description = `Test totals for iteration ${i}`;
getMetric("Geomean", "ms").description = "Geomean of test totals";
getMetric("Score", "score").description = "Scaled inverse of the Geomean";
}
const geomean = getMetric("Geomean");
const iterationTotal = iterationTotalMetric(geomean.length);
for (const results of Object.values(iterationResults))
iterationTotal.add(results.total);
iterationTotal.computeAggregatedMetrics();
geomean.add(iterationTotal.geomean);
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
for (const metric of Object.values(this._metrics))
metric.computeAggregatedMetrics();
}
}

View file

@ -0,0 +1,78 @@
# Chart benchmarks
## How to run locally
This project uses `npm` to manage dependencies and run scripts. It has been
tested with node v18 but should work with other versions of node.
This uses [vite](https://vitejs.dev/) as builder but this should be fairly
transparent.
### Install dependencies
```
npm i
```
### Run the development server
```
npm run dev
```
### Build the app
```
npm run build
```
### Preview the production build
```
npm run preview
```
## Included benchmarks
All the benchmarks are included in iframes in the index page. But it may be more
convenient to open these benchmarks with their specific files.
### [Observable Plot](https://github.com/observablehq/plot)
You can load this benchmark with the `/observable-plot.html` page, for example
http://localhost:5173/observable-plot.html if you run it locally or
http://localhost:8900/resources/charts/dist/observable-plot.html in the
context of the speedometer.
Observable Plot is D3-based and outputs SVG.
When run in development mode, the page will automatically execute the 3
included graphs:
- a stacked bar graph
- a grouped bar graph
- a graph using dots
In production mode nothing executes by default, the user needs to push the
buttons to run any code. That's how the benchmark exercizes this code.
To build these graphs, we use datasets representing flight information in the
US. You can consult them in the [datasets directory](./datasets).
### [ChartJS](https://github.com/chartjs/Chart.js)
You can load this benchmark with the `/chartjs.html` page, for example
http://localhost:5173/chartjs.html if you run it locally or
http://localhost:8900/resources/charts/dist/chartjs.html in the
context of speedometer.
ChartJS is canvas-based.
When run in development mode, the page will automatically execute the scatter
graph.
In production mode nothing executes by default, the user needs to push the
buttons to run any code. That's how the benchmark exercizes this code.
To build these graphs, we use datasets representing flight information in the
US. You can consult them in the [datasets directory](./datasets).

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chart.js</title>
<style type="text/css" media="all">
label {
user-select: none;
display: inline-flex;
align-items: center;
}
</style>
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
<button id="open-tooltip" type="button">Open tooltips</button>
<button id="reset" type="button">Reset</button>
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
</div>
<canvas id="chart"></canvas>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
<script type="module" src="/chartjs.js"></script>
</body>
</html>

174
resources/charts/chartjs.js Normal file
View file

@ -0,0 +1,174 @@
import Chart from "chart.js/auto";
import { csvParse } from "d3-dsv";
// These 2 imports use some vite machinery to directly import these files as
// strings. Then they are constants and this reduces the load on the GC.
import airportsString from "./datasets/airports.csv?raw";
import flightsString from "./datasets/flights-airports.csv?raw";
/*
* From https://www.omnicalculator.com/other/latitude-longitude-distance:
* d = 2R × sin⁻¹(√[sin²((θ₂ - θ₁)/2) + cosθ₁ × cosθ₂ × sin²((φ₂ - φ₁)/2)])
*
* where:
* θ₁, φ₁ – First point latitude and longitude coordinates;
* θ₂, φ₂ – Second point latitude and longitude coordinates;
* R – Earth's radius (R = 6371 km); and
* d – Distance between them along Earth's surface.
*/
const R = 6371;
function computeDistance(coords1, coords2) {
const long1 = toRadian(coords1.longitude);
const lat1 = toRadian(coords1.latitude);
const long2 = toRadian(coords2.longitude);
const lat2 = toRadian(coords2.latitude);
const longSquareSin = Math.sin((long2 - long1) / 2) ** 2;
const latSquareSin = Math.sin((lat2 - lat1) / 2) ** 2;
const d = 2 * R * Math.asin(Math.sqrt(latSquareSin + Math.cos(lat1) * Math.cos(lat2) * longSquareSin));
return d;
}
const RAD = Math.PI / 180;
function toRadian(deg) {
return deg * RAD;
}
let preparedData = null;
function prepare() {
/**
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
* airports: Array<AirportInformation>
* flights: Array<{ origin, destination, count }>
*/
const airports = csvParse(airportsString);
const flights = csvParse(flightsString);
const airportMap = new Map(airports.map((airport) => [airport.iata, airport]));
for (const flight of flights) {
const origAirport = airportMap.get(flight.origin);
const destAirport = airportMap.get(flight.destination);
flight.distance = computeDistance(origAirport, destAirport);
}
preparedData = { flights, airportMap };
}
const ROOT = document.getElementById("chart");
const opaqueCheckBox = document.getElementById("opaque-color");
let currentChart = null;
function drawScattered(data) {
if (!preparedData)
throw new Error("Please prepare the data first.");
reset();
currentChart = new Chart(ROOT, {
type: "scatter",
data: {
datasets: [
{
data: preparedData.flights,
backgroundColor: opaqueCheckBox.checked ? "rgb(0, 125, 255)" : "rgba(0, 125, 255, .20)",
},
],
},
options: {
animation: false,
parsing: {
xAxisKey: "distance",
yAxisKey: "count",
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: "Number of flights for a distance",
position: "bottom",
},
tooltip: {
displayColors: false,
callbacks: {
label: (item) => {
const orig = preparedData.airportMap.get(item.raw.origin);
const dest = preparedData.airportMap.get(item.raw.destination);
const result = `✈ ${orig.name} (${orig.iata}) → ${dest.name} (${dest.iata}): ${Math.round(item.raw.distance)} km`;
return result;
},
},
},
},
scales: {
x: {
title: {
text: "distance →",
align: "end",
display: true,
},
ticks: {
format: {
style: "unit",
unit: "kilometer",
},
},
},
y: {
type: "logarithmic",
title: {
text: "# of flights →",
align: "end",
display: true,
},
},
},
},
});
}
function openTooltip() {
if (!currentChart)
throw new Error("No chart is present, please draw a chart first");
const renderedDataset = currentChart.getDatasetMeta(0);
const node = currentChart.canvas;
const rect = node.getBoundingClientRect();
// Index 2426 is carefully chosen to display a lot of lines (which depends
// on the zoom level).
const point = renderedDataset.data[2426];
const event = new MouseEvent("mousemove", {
clientX: rect.left + point.x,
clientY: rect.top + point.y,
cancelable: true,
bubbles: true,
});
node.dispatchEvent(event);
}
function reset() {
if (currentChart) {
currentChart.destroy();
currentChart = null;
}
}
async function runAllTheThings() {
[
// Force prettier to use a multiline formatting
"prepare",
"add-scatter-chart-button",
"open-tooltip",
].forEach((id) => document.getElementById(id).click());
}
document.getElementById("prepare").addEventListener("click", prepare);
document.getElementById("add-scatter-chart-button").addEventListener("click", drawScattered);
document.getElementById("open-tooltip").addEventListener("click", openTooltip);
document.getElementById("reset").addEventListener("click", reset);
document.getElementById("run-all").addEventListener("click", runAllTheThings);
if (import.meta.env.DEV)
runAllTheThings();

View file

@ -0,0 +1,4 @@
Origin of the datasets:
airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/airports.csv
flights-airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/flights-airport.csv

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
<iframe src="./observable-plot.html"></iframe>
</details>
<details open>
<summary>
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
</summary>
<iframe src="./chartjs.html"></iframe>
</details>
</div>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"developer-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"index-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

35
resources/charts/dist/chartjs.html vendored Normal file
View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chart.js</title>
<style type="text/css" media="all">
label {
user-select: none;
display: inline-flex;
align-items: center;
}
</style>
<script type="module" crossorigin src="./assets/chartjs-7fd89fd7.js"></script>
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
<button id="open-tooltip" type="button">Open tooltips</button>
<button id="reset" type="button">Reset</button>
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
</div>
<canvas id="chart"></canvas>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
</body>
</html>

33
resources/charts/dist/developer.html vendored Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
<iframe src="./observable-plot.html"></iframe>
</details>
<details open>
<summary>
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
</summary>
<iframe src="./chartjs.html"></iframe>
</details>
</div>
</body>
</html>

15
resources/charts/dist/index.html vendored Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<body>
<h1>Charting workloads</h1>
<ul>
<li><a href="./observable-plot.html">Observable Plot</a></li>
<li><a href="./chartjs.html">Chart.js</a></li>
</ul>
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Observable Plot</title>
<style>
label {
display: inline-flex;
align-content: center;
gap: 3px;
margin: 0 3px;
}
#airport-group-size {
min-width: 30px;
}
</style>
<script type="module" crossorigin src="./assets/plot-562fbdb6.js"></script>
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
<button id="reset" type="button">Reset</button>
<label>
Number of airports to keep in each group:
<span id="airport-group-size"></span>
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
</label>
</div>
<div id="chart"></div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<body>
<h1>Charting workloads</h1>
<ul>
<li><a href="./observable-plot.html">Observable Plot</a></li>
<li><a href="./chartjs.html">Chart.js</a></li>
</ul>
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
</body>
</html>

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Observable Plot</title>
<style>
label {
display: inline-flex;
align-content: center;
gap: 3px;
margin: 0 3px;
}
#airport-group-size {
min-width: 30px;
}
</style>
</head>
<body>
<div id="app">
<button id="run-all" type="button">Run all scenario (development only)</button>
<button id="prepare" type="button">Prepare data</button>
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
<button id="reset" type="button">Reset</button>
<label>
Number of airports to keep in each group:
<span id="airport-group-size"></span>
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
</label>
</div>
<div id="chart"></div>
<script type="module" src="/observable-plot.js"></script>
</body>
</html>

View file

@ -0,0 +1,233 @@
import * as Plot from "@observablehq/plot";
import { csvParse } from "d3-dsv";
import * as d3Array from "d3-array";
import { format as d3Format } from "d3-format";
// These 2 imports use some vite machinery to directly import these files as
// strings. Then they are constants and this reduces the load on the GC.
import airportsString from "./datasets/airports.csv?raw";
import flightsString from "./datasets/flights-airports.csv?raw";
const ROOT = document.getElementById("chart");
const DEFAULT_OPTIONS = {
// This width is really a max-width: the plot adjusts using the available
// width as well.
width: 2000,
// The height is especially used for the ratio.
height: 1000,
};
let preparedData;
function prepare() {
/**
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
* airports: Array<AirportInformation>
* flights: Array<{ origin, destination, count }>
*/
const { airports, flights } = parseAirportsInformation();
/**
* flightsByAirport: Map<iata: string, { origin: number, destination: number, total: number}>
*/
const flightsByAirport = groupFlightsByAirports(flights);
/**
* byAirport: Map<iata: string, AirportInformation>
*/
const byAirport = d3Array.index(airports, (d) => d.iata);
/* Array<[state, AirportInformation[]]> */
const airportsGroupedByStateArray = d3Array.groups(airports, (d) => d.state);
/* DescSortedArray<[{ state: string, total: number, mostUsedAirportsInState: AirportInformation[] }]> */
const stateInformationSortedArray = airportsGroupedByStateArray
.map(([state, airportsInState]) => {
const totalFlightsInState = d3Array.sum(airportsInState, ({ iata }) => flightsByAirport.get(iata)?.total);
const sorted = d3Array.sort(airportsInState, ({ iata }) => -flightsByAirport.get(iata)?.total);
const mostUsedAirportsInState = sorted.slice(0, airportCountPerGroup());
return {
state,
total: totalFlightsInState,
mostUsedAirports: mostUsedAirportsInState,
};
})
.sort((stateA, stateB) => stateB.total - stateA.total);
/* Array<state: string> */
const stateSortedArray = stateInformationSortedArray.map(({ state }) => state);
// Flatten the information in preparedData.stateInformationSortedArray, so that we
// have one item == one airport information.
/* Array<{state, iata, name, city, index, origin, destination, total}> */
const plotData = stateInformationSortedArray.flatMap(({ mostUsedAirports, total, state }) => {
const enrichedMostUsedAirports = mostUsedAirports.map(({ iata, name, city }, index) => ({
state,
iata,
name,
city,
index, // This will be used to have consistent colors.
...flightsByAirport.get(iata),
}));
const otherTotal = total - d3Array.sum(mostUsedAirports, ({ iata }) => flightsByAirport.get(iata)?.total);
if (otherTotal > 0) {
enrichedMostUsedAirports.push({
state,
iata: "Other",
total: otherTotal,
index: enrichedMostUsedAirports.length,
});
}
return enrichedMostUsedAirports;
});
preparedData = {
airports,
flights,
flightsByAirport,
byAirport,
stateInformationSortedArray,
stateSortedArray,
plotData,
};
}
function parseAirportsInformation() {
return {
airports: csvParse(airportsString),
flights: csvParse(flightsString),
};
}
function groupFlightsByAirports(flights) {
const flightsByAirport = new Map();
for (const { origin, destination, count } of flights) {
const infoForOriginAirport = flightsByAirport.get(origin) ?? { origin: 0, destination: 0, total: 0 };
const intCount = Number(count);
if (Number.isNaN(intCount)) {
console.error(`Couldn't convert ${count} to number.`);
continue;
}
infoForOriginAirport.origin += intCount;
infoForOriginAirport.total += intCount;
flightsByAirport.set(origin, infoForOriginAirport);
const infoForDestinationAirport = flightsByAirport.get(destination) ?? { origin: 0, destination: 0, total: 0 };
infoForDestinationAirport.destination += intCount;
infoForDestinationAirport.total += intCount;
flightsByAirport.set(destination, infoForDestinationAirport);
}
return flightsByAirport;
}
function isReady() {
return !!preparedData;
}
function addStackedBars() {
if (!isReady())
throw new Error("Please prepare the data first.");
const options = {
...DEFAULT_OPTIONS,
color: { type: "categorical" },
x: {
domain: preparedData.stateSortedArray,
},
y: { grid: true, tickFormat: "~s" },
marks: [
Plot.barY(preparedData.plotData, {
x: "state",
y: "total",
fill: "index",
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(d.total)} flights`,
}),
Plot.text(preparedData.stateInformationSortedArray, { x: "state", y: "total", text: (d) => d3Format(".2~s")(d.total), dy: -10 }),
Plot.ruleY([0]),
],
};
ROOT.append(Plot.plot(options));
}
function addDottedBars() {
if (!isReady())
throw new Error("Please prepare the data first.");
const data = [...preparedData.flightsByAirport]
.flatMap(([iata, { origin, destination }]) => {
const airportInformation = preparedData.byAirport.get(iata);
return [
{ ...airportInformation, value: -origin },
{ ...airportInformation, value: destination },
];
})
.filter((d) => d.value);
const options = {
...DEFAULT_OPTIONS,
color: { type: "threshold", domain: [0] },
x: {
domain: preparedData.stateSortedArray,
},
y: {
grid: true,
label: "← outward Number of flights inward →",
labelAnchor: "center",
tickFormat: (v) => d3Format("~s")(Math.abs(v)),
type: "pow",
exponent: 0.2,
},
marks: [
Plot.dot(data, {
x: "state",
y: "value",
r: 4,
stroke: "value",
strokeWidth: 3,
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(Math.abs(d.value))} ${d.value > 0 ? "inward" : "outward"} flights`,
}),
Plot.ruleY([0]),
],
};
ROOT.append(Plot.plot(options));
}
function reset() {
ROOT.textContent = "";
}
async function runAllTheThings() {
reset();
[
// Force prettier to use a multiline formatting
"prepare",
"add-stacked-chart-button",
"add-dotted-chart-button",
].forEach((id) => document.getElementById(id).click());
}
// This is the number of airports we keep for each state in the stacked bar
// graph. One additional group will be added, that will sum all airports that
// haven't been kept. It's retrieved from the input directly.
function airportCountPerGroup() {
return document.querySelector("#airport-group-size-input").value;
}
function onGroupSizeInputChange() {
document.querySelector("#airport-group-size").textContent = airportCountPerGroup();
if (import.meta.env.DEV) {
// In dev mode, redraw everything
runAllTheThings();
}
}
document.getElementById("prepare").addEventListener("click", prepare);
document.getElementById("add-stacked-chart-button").addEventListener("click", addStackedBars);
document.getElementById("add-dotted-chart-button").addEventListener("click", addDottedBars);
document.getElementById("reset").addEventListener("click", reset);
document.getElementById("run-all").addEventListener("click", runAllTheThings);
document.getElementById("airport-group-size-input").addEventListener("input", onGroupSizeInputChange);
onGroupSizeInputChange();
if (import.meta.env.DEV)
runAllTheThings();

1022
resources/charts/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
{
"name": "charts-benchmark",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.1.0"
},
"dependencies": {
"@observablehq/plot": "^0.6.4",
"chart.js": "^4.2.1",
"d3-dsv": "^3.0.1"
}
}

View file

@ -0,0 +1,18 @@
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
base: "./", // Since this will be loaded from the project root
build: {
modulePreload: { polyfill: false },
minify: false,
sourcemap: true,
rollupOptions: {
input: {
index: resolve(__dirname, "index.html"),
developer: resolve(__dirname, "developer.html"),
plot: resolve(__dirname, "observable-plot.html"),
chartjs: resolve(__dirname, "chartjs.html"),
},
},
},
});

View file

@ -0,0 +1,286 @@
import { Suites, Tags } from "./tests.mjs";
import { params, defaultParams } from "./shared/params.mjs";
export function createDeveloperModeContainer() {
const container = document.createElement("div");
container.className = "developer-mode";
const details = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = "Developer Mode";
details.append(summary);
const content = document.createElement("div");
content.className = "developer-mode-content";
content.append(createUIForSuites());
const settings = document.createElement("div");
settings.className = "settings";
settings.append(createUIForIterationCount());
settings.append(createUIForWarmupSuite());
settings.append(createUIForWarmupBeforeSync());
settings.append(createUIForSyncStepDelay());
settings.append(createUIForAsyncSteps());
settings.append(createUIForLeakyIframes());
content.append(document.createElement("hr"));
content.append(settings);
content.append(document.createElement("hr"));
content.append(createUIForRun());
details.append(content);
container.append(details);
return container;
}
function span(text) {
const span = document.createElement("span");
span.textContent = text;
return span;
}
function createUIForWarmupSuite() {
return createCheckboxUI("Use Warmup Suite", params.useWarmupSuite, (isChecked) => {
params.useWarmupSuite = isChecked;
});
}
function createUIForAsyncSteps() {
return createCheckboxUI("Use Async Steps", params.useAsyncSteps, (isChecked) => {
params.useAsyncSteps = isChecked;
});
}
function createUIForLeakyIframes() {
return createCheckboxUI("Leak IFrames", params.leakyIframes, (isChecked) => {
params.leakyIframes = isChecked;
});
}
function createCheckboxUI(labelValue, initialValue, paramsUpdateCallback) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = !!initialValue;
checkbox.onchange = () => {
paramsUpdateCallback(checkbox.checked);
updateURL();
};
const label = document.createElement("label");
label.append(checkbox, " ", span(labelValue));
return label;
}
function createUIForIterationCount() {
return createTimeRangeUI("Iterations: ", "iterationCount", "#", 1, 200);
}
function createUIForWarmupBeforeSync() {
return createTimeRangeUI("Warmup time: ", "warmupBeforeSync");
}
function createUIForSyncStepDelay() {
return createTimeRangeUI("Sync step delay: ", "waitBeforeSync");
}
function createTimeRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000) {
const range = document.createElement("input");
range.type = "range";
range.min = min;
range.max = max;
range.value = params[paramKey];
const rangeValueAndUnit = document.createElement("span");
rangeValueAndUnit.className = "range-label-data";
const rangeValue = document.createElement("span");
rangeValue.textContent = params[paramKey];
rangeValueAndUnit.append(rangeValue, " ", unit);
const label = document.createElement("label");
label.append(span(labelText), range, rangeValueAndUnit);
range.oninput = () => {
rangeValue.textContent = range.value;
};
range.onchange = () => {
params[paramKey] = parseInt(range.value);
updateURL();
};
return label;
}
function createUIForSuites() {
const control = document.createElement("nav");
control.className = "suites";
const checkboxes = [];
const setSuiteEnabled = (suiteIndex, enabled) => {
Suites[suiteIndex].disabled = !enabled;
checkboxes[suiteIndex].checked = enabled;
};
control.appendChild(createSuitesGlobalSelectButtons(setSuiteEnabled));
const ol = document.createElement("ol");
for (const suite of Suites) {
const li = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.id = suite.name;
checkbox.type = "checkbox";
checkbox.checked = !suite.disabled;
checkbox.onchange = () => {
suite.disabled = !checkbox.checked;
updateURL();
};
checkboxes.push(checkbox);
const label = document.createElement("label");
label.append(checkbox, " ", suite.name);
li.appendChild(label);
label.onclick = (event) => {
if (event?.ctrlKey || event?.metaKey) {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
if (Suites[suiteIndex] !== suite)
setSuiteEnabled(suiteIndex, false);
else
setSuiteEnabled(suiteIndex, true);
}
}
};
ol.appendChild(li);
}
control.appendChild(ol);
control.appendChild(createSuitesTagsButton(setSuiteEnabled));
return control;
}
function createSuitesGlobalSelectButtons(setSuiteEnabled) {
const buttons = document.createElement("div");
buttons.className = "button-bar";
let button = document.createElement("button");
button.className = "select-all";
button.textContent = "Select all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, true);
updateURL();
};
buttons.appendChild(button);
button = document.createElement("button");
button.textContent = "Unselect all";
button.className = "unselect-all";
button.onclick = () => {
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
setSuiteEnabled(suiteIndex, false);
updateURL();
};
buttons.appendChild(button);
return buttons;
}
function createSuitesTagsButton(setSuiteEnabled) {
let tags = document.createElement("div");
let buttons = tags.appendChild(document.createElement("div"));
buttons.className = "button-bar";
let i = 0;
const kTagsPerLine = 3;
for (const tag of Tags) {
if (tag === "all")
continue;
if (!(i % kTagsPerLine)) {
buttons = tags.appendChild(document.createElement("div"));
buttons.className = "button-bar";
}
i++;
const button = document.createElement("button");
button.className = "tag";
button.textContent = `#${tag}`;
button.dataTag = tag;
button.onclick = (event) => {
const extendSelection = event?.shiftKey;
const invertSelection = event?.ctrlKey || event?.metaKey;
const selectedTag = event.target.dataTag;
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
let enabled = Suites[suiteIndex].tags.includes(selectedTag);
if (invertSelection)
enabled = !enabled;
if (extendSelection && !enabled)
continue;
setSuiteEnabled(suiteIndex, enabled);
}
updateURL();
};
buttons.appendChild(button);
}
return tags;
}
function createUIForRun() {
const stepTestButton = document.createElement("button");
stepTestButton.textContent = "Step Test \u23EF";
stepTestButton.onclick = (event) => {
globalThis.benchmarkClient.step();
};
const startTestButton = document.createElement("button");
startTestButton.textContent = "Start Test \u23F5";
startTestButton.onclick = (event) => {
globalThis.benchmarkClient.start();
};
const buttons = document.createElement("div");
buttons.className = "button-bar";
buttons.appendChild(stepTestButton);
buttons.appendChild(startTestButton);
return buttons;
}
function updateURL() {
const url = new URL(window.location.href);
// If less than all suites are selected then change the URL "Suites" GET parameter
// to comma separate only the selected
const selectedSuites = Suites.filter((suite) => !suite.disabled);
url.searchParams.delete("tags");
url.searchParams.delete("suites");
url.searchParams.delete("suite");
if (selectedSuites.length) {
// Try finding common tags that would result in the current suite selection.
let commonTags = new Set(selectedSuites[0].tags);
for (const suite of Suites) {
if (suite.disabled)
suite.tags.forEach((tag) => commonTags.delete(tag));
else
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
}
if (selectedSuites.length > 1 && commonTags.size) {
const tags = [...commonTags][0];
if (tags !== "default")
url.searchParams.set("tags", tags);
url.searchParams.delete("suites");
} else {
url.searchParams.set("suites", selectedSuites.map((suite) => suite.name).join(","));
}
}
const defaultParamKeys = ["iterationCount", "useWarmupSuite", "warmupBeforeSync", "waitBeforeSync", "useAsyncSteps", "leakyIframes"];
for (const paramKey of defaultParamKeys) {
if (params[paramKey] !== defaultParams[paramKey])
url.searchParams.set(paramKey, params[paramKey]);
else
url.searchParams.delete(paramKey);
}
// Only push state if changed
url.search = decodeURIComponent(url.search);
if (url.href !== window.location.href)
window.history.pushState({}, "", url);
}

26
resources/editors/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,27 @@
## Description
Lots of people edit text content in the browser. Lots of that content, like WYSIWYG content or code, is too rich or complex to represent well with a `<textarea>`. Sites typically rely on advanced editor libraries for this, and we should make sure browsers perform well at common patterns used by them.
## Screenshot
![screenshot](./screenshot.jpg)
## What are we testing
- Basic DOM and editing
- Virtualization (DOM content changing during scroll)
- Basic flex layout with SVG icons
## How are we testing
The test simulates a real-world user flow by loading a number of popular editor libraries. After the initial load is complete, the following steps are timed:
- Setting to a fairly large value
- "Formatting" the text - in code editors this means turning on syntax highlighting, and in WYSISWYG this means bolding all of the contents
- Scrolling to the bottom of the editor
## Developer Documentation
The app was created with `npm create vite@latest editors`, and can be previewed with `npm run dev`. In order to update the files run in the harness you have to use `npm run build` which will recreate the `dist/` directory.
The built test can be loaded within the harness using i.e. http://localhost:8900/?suite=Editor-CodeMirror&startAutomatically=true, or the static versions at http://localhost:7000/resources/editors/dist/.

View file

@ -0,0 +1,2 @@
longtext.html is "Du côté de chez Swann" by Marcel Proust, downloaded from https://www.gutenberg.org/ebooks/2650
longscript.js has been downloaded from https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>CodeMirror Test</title>
<script>
window.requestIdleCallback = undefined;
window.cancelIdleCallback = undefined;
</script>
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
<script type="module">
import { code as shorttext } from "./shorttext.js";
import { code as longtext } from "./longtext.js";
import editor from "./codemirror.js";
let editorContainer = document.querySelector("#editor");
let editorInstance = null;
let buttons = {
create: document.querySelector("#create"),
highlight: document.querySelector("#highlight"),
unhighlight: document.querySelector("#unhighlight"),
long: document.querySelector("#long"),
short: document.querySelector("#short"),
scroll: document.querySelector("#scroll"),
layout: document.querySelector("#layout"),
};
buttons.scroll.addEventListener("click", scroll);
buttons.highlight.addEventListener("click", highlight);
buttons.unhighlight.addEventListener("click", unhighlight);
buttons.long.addEventListener("click", long);
buttons.short.addEventListener("click", short);
buttons.layout.addEventListener("click", layout);
buttons.create.addEventListener("click", (e) => {
if (!editorInstance) {
editorInstance = editor(editorContainer);
editorInstance.ready.then(() => {
buttons.unhighlight.classList.add("active", "true");
buttons.create.setAttribute("disabled", "true");
});
}
});
function layout() {
// Todo - is this necessary with the runner?
const body = document.body.getBoundingClientRect();
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
}
function highlight() {
buttons.unhighlight.classList.toggle("active", false);
buttons.highlight.classList.toggle("active", true);
editorInstance.format(true);
}
function unhighlight() {
buttons.unhighlight.classList.toggle("active", true);
buttons.highlight.classList.toggle("active", false);
editorInstance.format(false);
}
function long() {
buttons.short.classList.toggle("active", false);
buttons.long.classList.toggle("active", true);
editorInstance.setValue(longtext);
}
function short() {
buttons.short.classList.toggle("active", true);
buttons.long.classList.toggle("active", false);
editorInstance.setValue(shorttext);
}
function scroll() {
let isTop = editorInstance.getScrollTop() == 0;
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
}
</script>
</body>
</html>

View file

@ -0,0 +1,47 @@
// https://codemirror.net/examples/bundle/
import { EditorView, basicSetup } from "codemirror";
import { StateEffect } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
let lang = javascript();
let extensions = [basicSetup, EditorView.lineWrapping];
export default function (element, value) {
let view = new EditorView({
extensions,
parent: element,
doc: value,
wordWrapColumn: 80,
});
return {
editor: view,
// Anything before this promise resolves will happen before timing starts
ready: Promise.resolve(),
getScrollHeight() {
return element.scrollHeight;
},
getScrollTop() {
return element.scrollTop;
},
setScrollTop(value) {
element.scrollTop = value;
},
setValue: (value) =>
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
}),
format(on) {
// https://codemirror.net/examples/config/
// https://discuss.codemirror.net/t/cm6-dynamically-switching-syntax-theme-w-reconfigure/2858/6
if (on && extensions.length === 2)
extensions.push(lang);
else if (!on && extensions.length === 3)
extensions.pop();
view.dispatch({
effects: StateEffect.reconfigure.of(extensions),
});
},
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,66 @@
:root {
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
line-height: 1.5;
font-weight: 400;
font-size: 13px;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
text-size-adjust: none;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #747bff;
}
body {
margin: 0;
display: flex;
min-width: 320px;
height: 100vh;
padding: 5px;
box-sizing: border-box;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
flex: 1;
display: flex;
flex-direction: column;
}
#editor {
flex: 1;
overflow: auto;
padding: 2px 5px;
}
#controls {
display: flex;
padding-bottom: 5px;
}
#controls button {
display: flex;
align-items: center;
margin: 0 2px;
padding: 2px;
}
#controls button.active {
color: blue;
}

View file

@ -0,0 +1,129 @@
const style = "";
const code$1 = `
function x() {
console.log("Hello world!");
}
`;
const text = `
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
`;
var base = {
8: "Backspace",
9: "Tab",
10: "Enter",
12: "NumLock",
13: "Enter",
16: "Shift",
17: "Control",
18: "Alt",
20: "CapsLock",
27: "Escape",
32: " ",
33: "PageUp",
34: "PageDown",
35: "End",
36: "Home",
37: "ArrowLeft",
38: "ArrowUp",
39: "ArrowRight",
40: "ArrowDown",
44: "PrintScreen",
45: "Insert",
46: "Delete",
59: ";",
61: "=",
91: "Meta",
92: "Meta",
106: "*",
107: "+",
108: ",",
109: "-",
110: ".",
111: "/",
144: "NumLock",
145: "ScrollLock",
160: "Shift",
161: "Shift",
162: "Control",
163: "Control",
164: "Alt",
165: "Alt",
173: "-",
186: ";",
187: "=",
188: ",",
189: "-",
190: ".",
191: "/",
192: "`",
219: "[",
220: "\\",
221: "]",
222: "'"
};
var shift = {
48: ")",
49: "!",
50: "@",
51: "#",
52: "$",
53: "%",
54: "^",
55: "&",
56: "*",
57: "(",
59: ":",
61: "+",
173: "_",
186: ":",
187: "+",
188: "<",
189: "_",
190: ">",
191: "?",
192: "~",
219: "{",
220: "|",
221: "}",
222: '"'
};
var chrome = typeof navigator != "undefined" && /Chrome\/(\d+)/.exec(navigator.userAgent);
var mac = typeof navigator != "undefined" && /Mac/.test(navigator.platform);
var ie = typeof navigator != "undefined" && /MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);
var brokenModifierNames = mac || chrome && +chrome[1] < 57;
for (var i = 0; i < 10; i++)
base[48 + i] = base[96 + i] = String(i);
for (var i = 1; i <= 24; i++)
base[i + 111] = "F" + i;
for (var i = 65; i <= 90; i++) {
base[i] = String.fromCharCode(i + 32);
shift[i] = String.fromCharCode(i);
}
for (var code in base)
if (!shift.hasOwnProperty(code))
shift[code] = base[code];
function keyName(event) {
var ignoreKey = brokenModifierNames && (event.ctrlKey || event.altKey || event.metaKey) || ie && event.shiftKey && event.key && event.key.length == 1 || event.key == "Unidentified";
var name = !ignoreKey && event.key || (event.shiftKey ? shift : base)[event.keyCode] || event.key || "Unidentified";
if (name == "Esc")
name = "Escape";
if (name == "Del")
name = "Delete";
if (name == "Left")
name = "ArrowLeft";
if (name == "Up")
name = "ArrowUp";
if (name == "Right")
name = "ArrowRight";
if (name == "Down")
name = "ArrowDown";
return name;
}
export {
base as b,
code$1 as c,
keyName as k,
shift as s,
text as t
};
//# sourceMappingURL=index.es-02a92ebc.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"main-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

144
resources/editors/dist/codemirror.html vendored Normal file
View file

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeMirror Test</title>
<script>
window.requestIdleCallback = undefined;
window.cancelIdleCallback = undefined;
</script>
<script type="module" crossorigin src="./assets/codemirror-521de7ab.js"></script>
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
<link rel="stylesheet" href="./assets/index-2feebe42.css">
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
<script>
// This hack allows to capture the work normally happening in a rAF. We
// may be able to remove it if the runner improves.
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
window.cancelAnimationFrame = window.clearTimeout;
</script>
</body>
</html>

32
resources/editors/dist/index.html vendored Normal file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Code Editors</strong></summary>
<iframe src="./codemirror.html"></iframe>
</details>
<details open>
<summary><strong>WYSIWYG Editors</strong></summary>
<iframe src="./tiptap.html"></iframe>
</details>
</div>
</body>
</html>

134
resources/editors/dist/tiptap.html vendored Normal file
View file

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TipTap Test</title>
<script type="module" crossorigin src="./assets/tiptap-95a40ba8.js"></script>
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
<link rel="stylesheet" href="./assets/index-2feebe42.css">
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
</body>
</html>

1
resources/editors/dist/vite.svg vendored Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Development UI</title>
<style>
iframe {
border: 0;
margin: 0;
padding: 0;
width: 48%;
height: 800px;
}
</style>
</head>
<body>
<div id="app">
<h1>Demo page (not intended for workload)</h1>
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
<details open>
<summary><strong>Code Editors</strong></summary>
<iframe src="./codemirror.html"></iframe>
</details>
<details open>
<summary><strong>WYSIWYG Editors</strong></summary>
<iframe src="./tiptap.html"></iframe>
</details>
</div>
</body>
</html>

View file

@ -0,0 +1,2 @@
export { default as code } from "./assets/longscript.js?raw";
export { default as text } from "./assets/longtext.html?raw";

2615
resources/editors/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{
"name": "editors",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@codemirror/lang-javascript": "^6.1.4",
"@tiptap/core": "^2.0.0-beta.217",
"@tiptap/starter-kit": "^2.0.0-beta.217",
"codemirror": "^6.0.1",
"vite": "^4.1.0"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,9 @@
export const code = `
function x() {
console.log("Hello world!");
}
`;
export const text = `
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
`;

View file

@ -0,0 +1,66 @@
:root {
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
line-height: 1.5;
font-weight: 400;
font-size: 13px;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
text-size-adjust: none;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #747bff;
}
body {
margin: 0;
display: flex;
min-width: 320px;
height: 100vh;
padding: 5px;
box-sizing: border-box;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
flex: 1;
display: flex;
flex-direction: column;
}
#editor {
flex: 1;
overflow: auto;
padding: 2px 5px;
}
#controls {
display: flex;
padding-bottom: 5px;
}
#controls button {
display: flex;
align-items: center;
margin: 0 2px;
padding: 2px;
}
#controls button.active {
color: blue;
}

View file

@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>TipTap Test</title>
</head>
<body>
<div id="app">
<div id="controls">
<button id="create">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l6 0"></path>
<path d="M12 9l0 6"></path>
</svg>
Create
</button>
&nbsp;
<button id="long">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Long text
</button>
<button id="short">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
stroke-width="0"
fill="currentColor"
></path>
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
</svg>
Short text
</button>
&nbsp;
<button id="highlight">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M21 15v4h-8l4 -4z"></path>
</svg>
Highlight
</button>
<button id="unhighlight">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-highlight-off"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
<path d="M12.5 5.5l4 4"></path>
<path d="M4.5 13.5l4 4"></path>
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
<path d="M3 3l18 18"></path>
</svg>
Unhighlight
</button>
&nbsp;
<button id="scroll">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 8l3 -3l3 3"></path>
<path d="M3 16l3 3l3 -3"></path>
<path d="M6 5l0 14"></path>
<path d="M13 6l7 0"></path>
<path d="M13 12l7 0"></path>
<path d="M13 18l7 0"></path>
</svg>
Scroll
</button>
&nbsp;
<button id="layout">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
</button>
</div>
<div id="editor"></div>
</div>
<script type="module">
import { text as shorttext } from "./shorttext.js";
import { text as longtext } from "./longtext.js";
import editor from "./tiptap.js";
let editorContainer = document.querySelector("#editor");
let editorInstance = null;
let buttons = {
create: document.querySelector("#create"),
highlight: document.querySelector("#highlight"),
unhighlight: document.querySelector("#unhighlight"),
long: document.querySelector("#long"),
short: document.querySelector("#short"),
scroll: document.querySelector("#scroll"),
layout: document.querySelector("#layout"),
};
buttons.scroll.addEventListener("click", scroll);
buttons.highlight.addEventListener("click", highlight);
buttons.unhighlight.addEventListener("click", unhighlight);
buttons.long.addEventListener("click", long);
buttons.short.addEventListener("click", short);
buttons.layout.addEventListener("click", layout);
buttons.create.addEventListener("click", (e) => {
if (!editorInstance) {
editorInstance = editor(editorContainer);
editorInstance.ready.then(() => {
buttons.unhighlight.classList.add("active", "true");
buttons.create.setAttribute("disabled", "true");
});
}
});
function layout() {
// Todo - is this necessary with the runner?
const body = document.body.getBoundingClientRect();
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
}
function highlight() {
buttons.unhighlight.classList.toggle("active", false);
buttons.highlight.classList.toggle("active", true);
editorInstance.format(true);
}
function unhighlight() {
buttons.unhighlight.classList.toggle("active", true);
buttons.highlight.classList.toggle("active", false);
editorInstance.format(false);
}
function long() {
buttons.short.classList.toggle("active", false);
buttons.long.classList.toggle("active", true);
editorInstance.setValue(longtext);
}
function short() {
buttons.short.classList.toggle("active", true);
buttons.long.classList.toggle("active", false);
editorInstance.setValue(shorttext);
}
function scroll() {
let isTop = editorInstance.getScrollTop() == 0;
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
}
</script>
</body>
</html>

View file

@ -0,0 +1,42 @@
// https://tiptap.dev/examples/default
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
export default function (element, value) {
let editor = new Editor({
element,
extensions: [StarterKit],
content: value,
editorProps: {
attributes: {
spellcheck: "false",
},
},
});
return {
editor,
// Anything before this promise resolves will happen before timing starts
ready: Promise.resolve(),
getScrollHeight() {
return element.scrollHeight;
},
getScrollTop() {
return element.scrollTop;
},
setScrollTop(value) {
element.scrollTop = value;
},
setValue(value) {
// Recommendation is to call focus before most commands
// https://tiptap.dev/api/commands#chain-commands
editor.chain().focus().setContent(value).setTextSelection(0).run();
element.scrollTop = 0;
},
format(on) {
if (on)
editor.chain().focus().selectAll().setBold().setTextSelection(0).run();
else
editor.chain().focus().selectAll().unsetBold().setTextSelection(0).run();
},
};
}

View file

@ -0,0 +1,17 @@
import { resolve } from "path";
import { defineConfig } from "vite";
export default defineConfig({
base: "./", // Since this will be loaded from the project root
build: {
modulePreload: { polyfill: false },
minify: false,
sourcemap: true,
rollupOptions: {
input: {
codemirror: resolve(__dirname, "codemirror.html"),
main: resolve(__dirname, "index.html"),
tiptap: resolve(__dirname, "tiptap.html"),
},
},
},
});

BIN
resources/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
resources/gauge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Some files were not shown because too many files have changed in this diff Show more