Simulators
Corpus runner
The documents that break an add-in are seldom the clean ones. officejs-harness corpus runs your suite once per .docx in a folder, each document on its own freshly seeded simulator, all at once. It prints a documents × tests matrix of passes, failures and syncs:
npx officejs-harness corpus ./fixtures -- npx vitest run
3 documents × 3 tests: 7 passed, 2 failed
1 fixtures/01-plain-contract.docx
2 fixtures/04-text-box.docx
3 fixtures/05-one-paragraph.docx
test/documents.test.js 1 2 3
reads every paragraph ✓ 2 ✓ 2 ✓ 2
walks to the second paragraph ✓ 1 ✓ 1 ✗ 1
search > finds the 30-day clause ✓ 1 ✗ 1 ✓ 1
syncs 4 4 4
✗ 2 fixtures/04-text-box.docx · test/documents.test.js · search > finds the 30-day clause
batch every batch answered without an error
error AssertionError: expected 0 to be greater than 0
rerun npx officejs-harness corpus fixtures/04-text-box.docx -- npx vitest run test/documents.test.js -t '^search finds the 30-day clause$'
log /tmp/officejs-corpus-k2Jd8Q/2-04-text-box.docx.log
✗ 3 fixtures/05-one-paragraph.docx · test/documents.test.js · walks to the second paragraph
batch sync 1 answered ItemNotFound at Paragraph.getNext: We couldn't find the item you requested. …
error RichApi.Error: We couldn't find the item you requested. …
rerun npx officejs-harness corpus fixtures/05-one-paragraph.docx -- npx vitest run test/documents.test.js -t '^walks to the second paragraph$'
log /tmp/officejs-corpus-k2Jd8Q/3-05-one-paragraph.docx.log
- A simulator per document. The runner starts one simulator per document it runs at once, with
rystic run word-twin -d --port 0, and stops them when it ends. Before each document it resets its simulator and seeds the document.--jobs Nsets how many run at once (one per CPU by default), and--twins http://127.0.0.1:8080,http://127.0.0.1:8081uses simulators you already run instead. The documents go only to those simulators, so they never leave your machine or runner. - Every test starts from the document. In a corpus run,
office.reset()resets the simulator and seeds that run’s document, so a suite that resets inbeforeEachruns unchanged. Your command also getsRYSTIC_TWIN_URLandOFFICEJS_HARNESS_DOCX, the document’s path. Your test files share the document’s simulator, so run them one file at a time (VitestfileParallelism: false, Jest--runInBand). - Each test, by name. For a
vitestorjestcommand, or annpm testscript that runs one, the runner adds a JSON reporter and reads each test’s outcome from it. Any other command is one row, passed or failed by its exit status. - Syncs per cell. A cell’s number is that test’s syncs on that document: the simulator’s count between one reset and the next. So a test that takes 3 syncs on a clean contract and 40 on a converted PDF shows it. A test that doesn’t reset gets no count of its own; the
syncsrow still totals the document. - A failure names its batch. When a batch returned an error, the cell names its sync since the reset, Word’s error code and the member that raised it. The
rerunline runs that test, in its file, on that document alone. - Exit status. The runner exits 0 when every test passed on every document and 1 otherwise, so CI fails on it. Ctrl-C stops the commands and the simulators, then exits 130. Each document’s output, report and batch log stay in the folder it prints, or in
--out <dir>.
Documents to put in your corpus
Add-ins break on a document’s structure. The simulator’s document engine is checked against documents of these nine shapes, every one fictional, on Word for the web and on the simulator, so they are good shapes to collect for your own corpus:
| Document | Shape |
|---|---|
| PDF converted by Word’s reflow | A framed letterhead, tabs and hard line breaks, a rebuilt table, a text box with a VML fallback, a section break |
| PDF converted by Acrobat’s export | Positioned shapes, a text box anchored to the page, exact line spacing, a framed date, words split across runs |
| Template | Content controls at block and inline level (plain text, drop-down, check box), a repeating section of table rows, a control around one cell |
| Fee schedule | A row spanning three columns, a cell merged down two rows, a table nested in a cell, empty cells |
| Exhibit cover sheet | Text boxes as DrawingML with VML fallbacks, anchored, inline and inside a table cell |
| Memo | PAGE, NUMPAGES and DATE fields as w:fldSimple and w:fldChar, bookmarks, a footnote, external and internal hyperlinks |
| Filing checklist | Wingdings symbols, tab leaders, soft and no-break hyphens, line breaks, a page break, a no-break space, an emoji |
| Pleading paper | Its own styles, a two-level numbered list, line numbers, a caption table, a footnote |
| Redlined schedule | Two authors’ tracked changes inside table cells and inside a content control, a comment on a cell’s text |
On each document, the checks compare:
body.paragraphstext, in order;- every paragraph, reached with
getNext; - a
searchfor a phrase in each place: a cell, a content control, a text box, a field result; - a range inside a table cell, and its
paragraphs; - a tracked edit inside a cell and inside a content control, read back as the document after.
The engine is also held to Word’s getOoxml of every paragraph, the body and each cell range on the same documents.
Last updated on