diff --git a/README.en.md b/README.en.md index 8194923..1d7e952 100644 --- a/README.en.md +++ b/README.en.md @@ -44,7 +44,7 @@ right here, with the URL, the header and the data never passing through our serv Tailwind CSS Zustand Open the app - Vitest + Vitest CI

@@ -65,7 +65,7 @@ self-hosted, straight from PostgreSQL/MySQL — one saved read-only query, and n and full-fidelity Excel/PDF export, where a chart exported to `.xlsx` is a real, editable chart bound to its cells, because the OOXML chart parts are written by hand (ExcelJS writes none). Plus optional bring-your-own-backend cloud save and live co-editing over it — presence, last-writer-wins with the loser told, and -an undo that does not erase the other person's work. Bilingual UI (Thai/English), 2013 automated tests. +an undo that does not erase the other person's work. Bilingual UI (Thai/English), 2014 automated tests. --- @@ -109,7 +109,7 @@ Want the harder parts: [embedding a Thai font in the PDF, with stacked tone mark --- -### 🧪 What 2013 passing tests could not catch +### 🧪 What 2014 passing tests could not catch Every test of the assistant **mocks the model** — it returns what I imagined it would. Put a real API key behind it, ask fourteen ordinary questions, and **six answers used functions this engine @@ -120,7 +120,7 @@ Then **the first fix made it worse.** The rule started as "give the closest form allows", so _"join all the names into one line"_ came back as `=SUM(A2:A20)` — `0` in the cell, no error, nothing to notice. **A visible `#NAME?` traded for an invisible wrong number.** -**And it happened again, in a different place.** With every gate green — 2013 tests, `axe` clean on +**And it happened again, in a different place.** With every gate green — 2014 tests, `axe` clean on both pages at two widths — an hour of clicking through the public build the way a first-time visitor would found three things no gate can see: @@ -395,7 +395,7 @@ Other available commands: | `npm run build` | Build a production bundle | | `npm run start` | Run the production build (run `npm run build` first) | | `npm run lint` | Check code quality with ESLint | -| `npm test` | Run the 2013-case Vitest suite | +| `npm test` | Run the 2014-case Vitest suite | | `npm run check:readme` | Check the READMEs still match the code (links/images/test count/new modules/both languages) | | `npm run check:screens` | Figures printed on a screenshot still match the source | | `npm run check:rls` | Two real accounts against your own Supabase: does the database refuse what the policies say it should (needs env) | @@ -1482,7 +1482,11 @@ ExcelJS writes no chart XML at all (`addImage` is its entire drawing API), so th are written by hand and spliced into the finished package; see `xlsxChartXml.ts` and `xlsxCharts.ts`. If that splice ever fails the export falls back to embedding pictures, because a package with a dangling relationship is one Excel calls corrupt and refuses to open — a much worse -outcome than a chart that has stopped updating. +outcome than a chart that has stopped updating. Each series refers to the column it was actually +read from (#59). A text column between the labels and the numbers, such as "Notes" between the +branch and Q1, is not a series but still takes up a column; the references used to be counted by +series, so Excel drew Notes where Q1 should be and Q1 where Q2 should be, while the app's own chart +was right. **Not supported:** charts in the PDF are still pictures (jsPDF has no chart primitive). A pie still draws one series at a time. The chart parts carry no colour or theme styling, so whichever app opens @@ -3041,7 +3045,7 @@ architecture behind it. | `@anthropic-ai/sdk` | Connects to the Claude API for the AI assistant | | `lucide-react` | UI icons | | `clsx` | Conditional className composition | -| `vitest` | Unit tests for the formula engine, sort logic, JSON-to-table conversion, pagination, rate limiting, templates, file fidelity, conditional formatting and live blocks (2013 cases) | +| `vitest` | Unit tests for the formula engine, sort logic, JSON-to-table conversion, pagination, rate limiting, templates, file fidelity, conditional formatting and live blocks (2014 cases) | > **Note:** No off-the-shelf formula library (e.g. HyperFormula) is used — the **formula engine is hand-written** > (tokenizer, parser, evaluator, and functions) to keep full control over its behavior. See @@ -4096,13 +4100,13 @@ the framework bundle itself, which isn't a trade worth making here. Written down ## 🧪 Testing ```bash -npm test # 2013 cases across 145 files, via Vitest +npm test # 2014 cases across 145 files, via Vitest ``` Testing is focused on the **formula engine, sort logic, JSON-to-table conversion, pagination, rate-limit backoff, Excel templates and live-block placement** — pure functions with no React/DOM dependency, so they run fast and give high confidence. -**But not one of those 2013 cases opens the app**, and nearly every bug this project found by hand lived in +**But not one of those 2014 cases opens the app**, and nearly every bug this project found by hand lived in the wiring *between* pieces that all passed their tests — the toolbar's "+ row" called `addRow`, which announced nothing, while `insertRowAtSelection` next to it announced correctly (both tested) · the AI assistant sent a range including its text header, because the context builder read raw `sheet.cells` @@ -4221,10 +4225,10 @@ once; disable `ArrowRight` in the grid and two assertions in the third fail. (Th second one stayed green: the `case` I inserted landed *after* the existing `case "ArrowRight"` and was dead code. Proving a gate means checking that the thing you meant to break actually broke.) -> **2013 tests passed, and 43% of the assistant's answers were unusable** — because those tests mock +> **2014 tests passed, and 43% of the assistant's answers were unusable** — because those tests mock > the model, so it returns what the test author imagined. A test count says what you thought to ask, > not whether you asked enough. Only a real API key found this: see -> [What 2013 passing tests could not catch](#-what-2013-passing-tests-could-not-catch), repeatable +> [What 2014 passing tests could not catch](#-what-2014-passing-tests-could-not-catch), repeatable > with `npm run check:ai`. | File | Cases | Tests | diff --git a/README.md b/README.md index 5722908..566cfd6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Tailwind CSS Zustand เปิดแอป - Vitest + Vitest CI

@@ -61,7 +61,7 @@ and saying so when data came back incomplete. A range can be given a name (in Th what it means, and a cell can carry a rule about what may go in it, refused before it is written rather than flagged afterwards. Conditional formatting re-colours cells from their current values — comparisons, top/bottom ranks, colour scales and data bars — and round-trips through Excel's own rule format. Bilingual UI -(Thai/English), 2013 automated tests. บันทึกบนคลาวด์และการแก้พร้อมกันหลายคนมีให้เลือกใช้ได้ โดยต่อ backend ของคุณเอง +(Thai/English), 2014 automated tests. บันทึกบนคลาวด์และการแก้พร้อมกันหลายคนมีให้เลือกใช้ได้ โดยต่อ backend ของคุณเอง --- @@ -106,7 +106,7 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' --- -### 🧪 สิ่งที่เทสต์ 2013 เคสจับไม่ได้ +### 🧪 สิ่งที่เทสต์ 2014 เคสจับไม่ได้ เทสต์ของผู้ช่วย AI ใช้ **mock ทั้งหมด** — มันตอบตามที่ผมคิดว่ามันจะตอบ พอเอา API key จริงมายิงคำถามธรรมดา 14 ข้อ **6 คำตอบใช้ฟังก์ชันที่เอนจินนี้ไม่มี** (`TEXTJOIN` `FIND` `RANK.EQ` `SUMPRODUCT` `CEILING` `CHAR`) @@ -116,7 +116,7 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' มันจึงตอบคำถาม *"ต่อชื่อทั้งหมดเป็นบรรทัดเดียว"* ด้วย `=SUM(A2:A20)` ซึ่งได้ `0` โดยไม่มีอะไรบอกว่าผิด **เปลี่ยน error ที่มองเห็น เป็นตัวเลขผิดที่มองไม่เห็น** -**แล้วมันก็เกิดอีก คนละที่** ตอนที่ทุกด่านเขียวหมด — 2013 เทสต์ผ่าน, `axe` ไม่มี violation ทั้งสองหน้า +**แล้วมันก็เกิดอีก คนละที่** ตอนที่ทุกด่านเขียวหมด — 2014 เทสต์ผ่าน, `axe` ไม่มี violation ทั้งสองหน้า ที่สองความกว้าง — ผมลองไล่คลิก build สาธารณะแบบคนที่เพิ่งเข้ามาครั้งแรกอยู่ชั่วโมงหนึ่ง แล้วเจอสามอย่าง ที่ไม่มีด่านไหนมองเห็น: @@ -381,7 +381,7 @@ npm run dev | `npm run build` | build เป็นเวอร์ชัน production | | `npm run start` | รันเวอร์ชันที่ build แล้ว (ต้อง `npm run build` ก่อน) | | `npm run lint` | ตรวจสอบคุณภาพโค้ดด้วย ESLint | -| `npm test` | รัน unit test 2013 เคสด้วย Vitest | +| `npm test` | รัน unit test 2014 เคสด้วย Vitest | | `npm run check:readme` | ตรวจว่า README ยังตรงกับโค้ด (ลิงก์/ภาพ/จำนวนเทสต์/โมดูลใหม่/สองภาษาตรงกัน) | | `npm run check:screens` | ตัวเลขที่พิมพ์อยู่บนภาพหน้าจอยังตรงกับซอร์สไหม | | `npm run check:rls` | ยิงจริงสองบัญชีบน Supabase ของคุณ ว่าฐานข้อมูลปฏิเสธตามที่ policy เขียนไว้ไหม (ต้องตั้ง env) | @@ -1306,6 +1306,9 @@ conditional formatting ไม่นับมันเป็นตัวเลข เราจึงเขียน OOXML chart part เอง แล้วแทรกลงในไฟล์ zip ที่ ExcelJS สร้างเสร็จ — ดู `xlsxChartXml.ts` กับ `xlsxCharts.ts` ถ้าการแทรกล้มเหลวจะถอยไปฝังเป็นรูปภาพแบบเดิม เพราะไฟล์ที่มี relationship ห้อย Excel จะบอกว่า "เสียหาย" แล้วเปิดไม่ขึ้นเลย ซึ่งแย่กว่ากราฟที่หยุดอัปเดตมาก +แต่ละชุดข้อมูลอ้างคอลัมน์ที่มันถูกอ่านมาจริง (#59) คอลัมน์ข้อความที่คั่นอยู่ เช่น "หมายเหตุ" ระหว่างชื่อสาขากับ Q1 +ไม่ใช่ชุดข้อมูลแต่ยังกินหนึ่งคอลัมน์ เดิมอ้างอิงนับตามลำดับชุด กราฟใน Excel จึงวาดหมายเหตุแทน Q1 และ Q1 แทน Q2 +ทั้งที่กราฟในแอปถูก **ยังไม่รองรับ:** กราฟใน PDF ยังเป็นรูปภาพ (jsPDF ไม่มีกราฟของตัวเอง) · กราฟวงกลมยังวาดได้ทีละชุดข้อมูล · ไม่ได้ใส่สี/ธีมลงใน chart part ปล่อยให้โปรแกรมที่เปิดใช้ค่าเริ่มต้นของตัวเอง @@ -2644,7 +2647,7 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans | `@anthropic-ai/sdk` | เชื่อมต่อ Claude API สำหรับผู้ช่วย AI | | `lucide-react` | ไอคอน UI | | `clsx` | รวม className แบบมีเงื่อนไข | -| `vitest` | unit test เอนจินคำนวณสูตร, ตรรกะเรียงข้อมูล, แปลง JSON เป็นตาราง, การแบ่งหน้า/rate limit, แม่แบบ/รูปแบบจากไฟล์ และบล็อกข้อมูลสด (2013 เคส) | +| `vitest` | unit test เอนจินคำนวณสูตร, ตรรกะเรียงข้อมูล, แปลง JSON เป็นตาราง, การแบ่งหน้า/rate limit, แม่แบบ/รูปแบบจากไฟล์ และบล็อกข้อมูลสด (2014 เคส) | > **หมายเหตุ:** ไม่ได้ใช้ไลบรารีคำนวณสูตรสำเร็จรูป (เช่น HyperFormula) แต่เขียน **เอนจินคำนวณสูตรขึ้นเอง** > ทั้ง tokenizer, parser, evaluator และฟังก์ชันต่างๆ เพื่อควบคุมพฤติกรรมได้เต็มที่ ดูรายละเอียดที่หัวข้อ @@ -3553,13 +3556,13 @@ B อ่านและแก้ได้ แต่ยังยึดเป็ ## 🧪 การทดสอบ ```bash -npm test # 2013 เคส ใน 145 ไฟล์ ด้วย Vitest +npm test # 2014 เคส ใน 145 ไฟล์ ด้วย Vitest ``` โฟกัสเทสต์ไปที่ **เอนจินคำนวณสูตร, ตรรกะเรียงข้อมูล, การแปลง JSON เป็นตาราง, การไล่ดึงหน้าถัดไป, การถอยเมื่อโดน rate limit, แม่แบบจากไฟล์ Excel, กฎจัดรูปแบบตามเงื่อนไข และการวางบล็อกข้อมูลสด** — ส่วนที่เป็น pure function ล้วน ไม่ต้องพึ่ง React/DOM จึงเทสต์ได้เร็วและมั่นใจได้สูง -**แต่ 2013 เคสนั้นไม่มีสักเคสที่เปิดแอป** และบั๊กที่โปรเจกต์นี้เจอด้วยการนั่งกดเอง อยู่ที่รอยต่อระหว่างชิ้นส่วนที่ผ่านเทสต์ +**แต่ 2014 เคสนั้นไม่มีสักเคสที่เปิดแอป** และบั๊กที่โปรเจกต์นี้เจอด้วยการนั่งกดเอง อยู่ที่รอยต่อระหว่างชิ้นส่วนที่ผ่านเทสต์ ทุกตัวแทบทั้งหมด — ปุ่ม "+ แถว" บนแถบเครื่องมือเรียก `addRow` ที่ไม่ประกาศอะไรเลย ขณะที่ `insertRowAtSelection` ข้าง ๆ ประกาศถูกต้อง (ทั้งคู่เทสต์ผ่าน) · ผู้ช่วย AI ส่งช่วงที่รวมหัวคอลัมน์ไปด้วย เพราะตัวประกอบ context อ่าน `sheet.cells` ที่เป็นสูตรดิบแทนค่าที่คำนวณแล้ว (ทั้งคู่เทสต์ผ่าน) · ปุ่มใหม่หนึ่งปุ่มดันปุ่มสลับภาษาตกขอบจอไป 42px @@ -3659,9 +3662,9 @@ seed ถูกปักไว้ ด่านที่แดงจึงแด ในตารางแล้วสองข้อในflow ที่สามตกทันที (ครั้งแรกที่ลองปิดยังเขียวอยู่ เพราะ `case` ที่แทรกเข้าไปไปอยู่หลัง `case "ArrowRight"` เดิม กลายเป็นโค้ดตาย — การพิสูจน์ต้องเช็กด้วยว่าของที่ตั้งใจทำให้พังนั้นพังจริง) -> **2013 เคสผ่านหมด แต่ 43% ของคำตอบจากผู้ช่วย AI ใช้ไม่ได้** — เพราะเทสต์ส่วนนั้น mock ตัวโมเดลไว้ +> **2014 เคสผ่านหมด แต่ 43% ของคำตอบจากผู้ช่วย AI ใช้ไม่ได้** — เพราะเทสต์ส่วนนั้น mock ตัวโมเดลไว้ > มันจึงตอบตามที่คนเขียนเทสต์คิดว่ามันจะตอบ จำนวนเทสต์บอกได้แค่ว่าเราถามอะไรไปบ้าง ไม่ได้บอกว่าเราถาม -> ครบหรือยัง เรื่องนี้เจอได้ก็ต่อเมื่อเอา API key จริงมายิง — ดู [สิ่งที่เทสต์ 2013 เคสจับไม่ได้](#-สิ่งที่เทสต์-2013-เคสจับไม่ได้) +> ครบหรือยัง เรื่องนี้เจอได้ก็ต่อเมื่อเอา API key จริงมายิง — ดู [สิ่งที่เทสต์ 2014 เคสจับไม่ได้](#-สิ่งที่เทสต์-2014-เคสจับไม่ได้) > และรันซ้ำเองได้ด้วย `npm run check:ai` | ไฟล์ | เคส | ทดสอบอะไร | diff --git a/public/screenshots/27-landing-stats.png b/public/screenshots/27-landing-stats.png index 8321550..68eac2d 100644 Binary files a/public/screenshots/27-landing-stats.png and b/public/screenshots/27-landing-stats.png differ diff --git a/public/screenshots/en/27-landing-stats.png b/public/screenshots/en/27-landing-stats.png index 081a6ce..343aeeb 100644 Binary files a/public/screenshots/en/27-landing-stats.png and b/public/screenshots/en/27-landing-stats.png differ diff --git a/public/screenshots/screenshots.json b/public/screenshots/screenshots.json index b9ad01a..af4988d 100644 --- a/public/screenshots/screenshots.json +++ b/public/screenshots/screenshots.json @@ -5,7 +5,7 @@ "27-landing-stats.png": { "paletteFormulas": 37, "engineFunctions": 66, - "tests": 2013, + "tests": 2014, "securityTests": 325 }, "38-landing-problems.png": { @@ -21,7 +21,7 @@ "en/27-landing-stats.png": { "paletteFormulas": 37, "engineFunctions": 66, - "tests": 2013, + "tests": 2014, "securityTests": 325 }, "en/38-landing-problems.png": { diff --git a/public/social-preview.png b/public/social-preview.png index 9352101..956e80b 100644 Binary files a/public/social-preview.png and b/public/social-preview.png differ diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index 88ff13f..bf61053 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -29,7 +29,7 @@ export default function OgImage() { const stats = [ ["66", "engine functions"], ["37", "palette formulas"], - ["2013", "automated tests"], + ["2014", "automated tests"], ["325", "of them security"], ["0", "formula libraries"], ]; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index c6579d3..4e113d6 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -432,7 +432,7 @@ export const en: Messages = { stats: [ { value: "37", label: "ready-made formulas" }, { value: "66", label: "engine functions" }, - { value: "2013", label: "automated tests" }, + { value: "2014", label: "automated tests" }, { value: "325", label: "security tests" }, { value: "0", label: "formula libraries used" }, ], diff --git a/src/i18n/th.ts b/src/i18n/th.ts index 5ac644b..c17e278 100644 --- a/src/i18n/th.ts +++ b/src/i18n/th.ts @@ -432,7 +432,7 @@ export const th: Messages = { stats: [ { value: "37", label: "สูตรพร้อมใช้" }, { value: "66", label: "ฟังก์ชันในเอนจิน" }, - { value: "2013", label: "เทสต์อัตโนมัติ" }, + { value: "2014", label: "เทสต์อัตโนมัติ" }, { value: "325", label: "เทสต์ด้านความปลอดภัย" }, { value: "0", label: "ไลบรารีคำนวณสูตร" }, ], diff --git a/src/lib/charts.test.ts b/src/lib/charts.test.ts index 05ec430..8aa412a 100644 --- a/src/lib/charts.test.ts +++ b/src/lib/charts.test.ts @@ -93,20 +93,20 @@ describe("reading a range into a chart", () => { describe("the value axis", () => { it("always includes zero, so a bar's length means what it looks like", () => { - expect(valueExtent([{ name: "a", points: [80, 100] }])).toEqual({ min: 0, max: 100 }); + expect(valueExtent([{ name: "a", column: 1, points: [80, 100] }])).toEqual({ min: 0, max: 100 }); }); it("extends below zero when the data does", () => { - expect(valueExtent([{ name: "a", points: [-30, 50] }])).toEqual({ min: -30, max: 50 }); + expect(valueExtent([{ name: "a", column: 1, points: [-30, 50] }])).toEqual({ min: -30, max: 50 }); }); it("never returns a zero-width span, which would divide by nothing when scaling", () => { - const flat = valueExtent([{ name: "a", points: [0, 0] }]); + const flat = valueExtent([{ name: "a", column: 1, points: [0, 0] }]); expect(flat.max).toBeGreaterThan(flat.min); }); it("handles a series with no numbers at all", () => { - const none = valueExtent([{ name: "a", points: [null, null] }]); + const none = valueExtent([{ name: "a", column: 1, points: [null, null] }]); expect(none.max).toBeGreaterThan(none.min); }); }); diff --git a/src/lib/charts.ts b/src/lib/charts.ts index fe5c0ee..47dd389 100644 --- a/src/lib/charts.ts +++ b/src/lib/charts.ts @@ -101,6 +101,10 @@ export function clampFrame(frame: ChartFrame, canvas: { width: number; height: n export interface ChartSeries { name: string; + /** Which column of the range it came from, counted from the range's first column. A column with + * no numbers is not a series, so this is not always the series' position after the labels, and + * an exported chart that assumed it was pointed at the wrong column (#59). */ + column: number; /** One value per label; a cell that holds no number becomes null and leaves a gap. */ points: (number | null)[]; } @@ -192,7 +196,7 @@ export function chartDataFrom(values: FormulaValue[][], range: SheetRange, dateT // A column with nothing numeric in it is not a series; plotting it would draw a flat nothing. if (points.every((p) => p === null)) continue; const headerName = usedHeaderRow ? cellText(cells[0][c]).trim() : ""; - series.push({ name: headerName || `${c - firstDataCol + 1}`, points }); + series.push({ name: headerName || `${c - firstDataCol + 1}`, column: c, points }); } // Labels name points; with nothing to plot they name nothing, so an empty range stays empty diff --git a/src/lib/xlsxChartXml.test.ts b/src/lib/xlsxChartXml.test.ts index 4cd3f1f..bccc840 100644 --- a/src/lib/xlsxChartXml.test.ts +++ b/src/lib/xlsxChartXml.test.ts @@ -132,8 +132,8 @@ describe("seriesRefsFrom", () => { const data: ChartData = { labels: ["Jan", "Feb", "Mar"], series: [ - { name: "Sales", points: [1, 2, 3] }, - { name: "Profit", points: [4, 5, 6] }, + { name: "Sales", column: 1, points: [1, 2, 3] }, + { name: "Profit", column: 2, points: [4, 5, 6] }, ], usedHeaderRow: true, usedLabelColumn: true, diff --git a/src/lib/xlsxChartXml.ts b/src/lib/xlsxChartXml.ts index 576bf9b..973b0fc 100644 --- a/src/lib/xlsxChartXml.ts +++ b/src/lib/xlsxChartXml.ts @@ -202,11 +202,10 @@ export function seriesRefsFrom( labelCol === null ? undefined : colRef(sheetName, columnLetter(labelCol), firstDataRow, lastDataRow); const chosen = pie ? data.series.slice(seriesIndex, seriesIndex + 1) : data.series; - return chosen.map((s, i) => { - const sourceIndex = pie ? seriesIndex : i; - // Series sit in the range's columns, after the label column when there is one. - const col = range.startCol + (data.usedLabelColumn ? 1 : 0) + sourceIndex; - const letter = columnLetter(col); + return chosen.map((s) => { + // The column the series was read from, not its position among the series: a text column + // between two numeric ones is skipped as a series but still takes up a column (#59). + const letter = columnLetter(range.startCol + s.column); return { name: s.name, nameRef: data.usedHeaderRow diff --git a/src/lib/xlsxCharts.test.ts b/src/lib/xlsxCharts.test.ts index 519543b..af0246c 100644 --- a/src/lib/xlsxCharts.test.ts +++ b/src/lib/xlsxCharts.test.ts @@ -74,6 +74,36 @@ describe("charts in the exported .xlsx", () => { expect(Object.keys(zip.files).some((n) => n.startsWith("xl/media/"))).toBe(false); }); + // #59: a text column between the labels and the numbers is not a series, but it still takes up + // a column. The references used to be counted from the series' position, so Q1 pointed at + // Notes and Q2 at Q1, and Excel, which reads the references, drew the wrong numbers. + it("points each series at its own column when a text column sits between them (#59)", async () => { + const sheet = createEmptySheet(); + const rows = [ + ["Branch", "Notes", "Q1", "Q2"], + ["North", "new", "120", "130"], + ["South", "ok", "150", "140"], + ["East", "late", "90", "95"], + ["West", "ok", "60", "70"], + ]; + rows.forEach((r, ri) => r.forEach((v, ci) => (sheet.cells[ri][ci] = v))); + const range = { startRow: 0, startCol: 0, endRow: 4, endCol: 3 }; + sheet.charts = [ + { id: "bar", kind: "bar", range, anchor: { col: 5, row: 1, dx: 0, dy: 0, w: 480, h: 300 } }, + { id: "pie", kind: "pie", range, seriesIndex: 1, anchor: { col: 5, row: 18, dx: 0, dy: 0, w: 480, h: 300 } }, + ]; + const zip = await exportZip([{ name: "Sheet1", sheet, computed: computeSheet(sheet) }]); + const bar = await zip.file("xl/charts/chart1.xml")!.async("string"); + expect(bar).toContain("Sheet1!$C$1"); + expect(bar).toContain("Sheet1!$C$2:$C$5"); + expect(bar).toContain("Sheet1!$D$1"); + expect(bar).toContain("Sheet1!$D$2:$D$5"); + expect(bar).not.toContain("$B$2:$B$5"); + const pie = await zip.file("xl/charts/chart2.xml")!.async("string"); + expect(pie).toContain("Sheet1!$D$2:$D$5"); + expect(pie).toContain("Sheet1!$A$2:$A$5"); + }); + // The failure this guards against is the worst one: a package with a relationship pointing at a // part that isn't there opens as "corrupt" with nothing saying which part is missing. it("leaves no dangling relationship anywhere in the package", async () => {