-
Notifications
You must be signed in to change notification settings - Fork 6
Add rich text support for CLI #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
80bcd1b
Add richText field to TextItemsResponse schema
asnewman 2006fe5
Add richText configuration option to base output filters
asnewman 3d2eb38
Update Text Items API request to support richText parameter
asnewman 4200b0b
Pass richText configuration to API filters in JSON formatter
asnewman 411de91
Use richText field in JSON output when configured and available
asnewman 8bc78d7
Add tests for richText field parsing in textItems
asnewman fd44d16
Use both top level and out filter
asnewman 9e6ccfc
formatting
asnewman 907b84a
remove unnecessary change
asnewman 35ebb5e
remove spec files
asnewman 828f0e8
clarify code
asnewman 6a86fae
simplify code
asnewman 45a4be0
remove incorrect richtext param
asnewman c74c117
add clarifying comment
asnewman 5d98a93
use boolean false instead of string
asnewman 26438da
Update url for finding your api key
marla-hoggard 5cb3302
Fix fetch text query params in fetchText
marla-hoggard 3fc9598
Move the changes higher up
marla-hoggard 26820d9
Add e2e tests
asnewman 3809f50
Merge branch 'ash/dit-11112-rich-text-for-cli' of github.com:dittowor…
asnewman ee8a815
Replace any types with proper TypeScript types in tests
asnewman 5347901
Move test variables and hooks inside describe block
asnewman 0a13b28
Use toSorted() instead of sort() in tests
asnewman 1708506
Simplify test setup by removing unused YAML config files
asnewman e47c9e2
Add proper filter tests with API parameter verification
asnewman bb48ae2
Split up filter/output tests, add more filter cases
marla-hoggard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,362 @@ | ||
| import { pull } from "./pull"; | ||
| import httpClient from "../http/client"; | ||
| import { TextItem } from "../http/textItems"; | ||
| import appContext from "../utils/appContext"; | ||
| import * as path from "path"; | ||
| import * as fs from "fs"; | ||
| import * as os from "os"; | ||
|
|
||
| jest.mock("../http/client"); | ||
|
|
||
| const mockHttpClient = httpClient as jest.Mocked<typeof httpClient>; | ||
|
|
||
| // Test data factories | ||
| const createMockTextItem = (overrides: Partial<TextItem> = {}) => ({ | ||
| id: "text-1", | ||
| text: "Plain text content", | ||
| richText: "<p>Rich <strong>HTML</strong> content</p>", | ||
| status: "active", | ||
| notes: "", | ||
| tags: [], | ||
| variableIds: [], | ||
| projectId: "project-1", | ||
| variantId: null, | ||
| ...overrides, | ||
| }); | ||
|
|
||
| const createMockVariable = (overrides: any = {}) => ({ | ||
| id: "var-1", | ||
| name: "Variable 1", | ||
| type: "string", | ||
| data: { | ||
| example: "variable value", | ||
| fallback: undefined, | ||
| }, | ||
| ...overrides, | ||
| }); | ||
|
|
||
| // Helper functions | ||
| const setupMocks = (textItems: TextItem[] = [], variables: any[] = []) => { | ||
| mockHttpClient.get.mockImplementation((url: string) => { | ||
| if (url.includes("/v2/textItems")) { | ||
| return Promise.resolve({ data: textItems }); | ||
| } | ||
| if (url.includes("/v2/variables")) { | ||
| return Promise.resolve({ data: variables }); | ||
| } | ||
| return Promise.resolve({ data: [] }); | ||
| }); | ||
| }; | ||
|
|
||
| const parseJsonFile = (filepath: string) => { | ||
| const content = fs.readFileSync(filepath, "utf-8"); | ||
| return JSON.parse(content); | ||
| }; | ||
|
|
||
| const assertFileContainsText = ( | ||
| filepath: string, | ||
| textId: string, | ||
| expectedText: string | ||
| ) => { | ||
| const content = parseJsonFile(filepath); | ||
| expect(content[textId]).toBe(expectedText); | ||
| }; | ||
|
|
||
| const assertFilesCreated = (outputDir: string, expectedFiles: string[]) => { | ||
| const actualFiles = fs.readdirSync(outputDir).toSorted(); | ||
| expect(actualFiles).toEqual(expectedFiles.toSorted()); | ||
| }; | ||
|
|
||
| describe("pull command - end-to-end tests", () => { | ||
| // Create a temporary directory for tests | ||
| let testDir: string; | ||
| let outputDir: string; | ||
|
|
||
| // Reset appContext before each test | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
|
|
||
| // Create a fresh temp directory for each test | ||
| testDir = fs.mkdtempSync(path.join(os.tmpdir(), "ditto-test-")); | ||
| outputDir = path.join(testDir, "output"); | ||
|
|
||
| // Reset appContext to a clean state | ||
| appContext.setProjectConfig({ | ||
| projects: [], | ||
| outputs: [ | ||
| { | ||
| format: "json", | ||
| outDir: outputDir, | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| // Clean up temp directory after each test | ||
| afterEach(() => { | ||
| if (testDir && fs.existsSync(testDir)) { | ||
| fs.rmSync(testDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| describe("Rich Text Feature", () => { | ||
| it("should use rich text when configured at base level", async () => { | ||
| // Only create output directory since we're mocking HTTP and setting appContext directly | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| const mockTextItem = createMockTextItem(); | ||
| setupMocks([mockTextItem], []); | ||
|
|
||
| // Set up appContext - this is what actually drives the test | ||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }], | ||
| richText: "html", | ||
| outputs: [{ format: "json", outDir: outputDir }], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify rich text content was written | ||
| assertFileContainsText( | ||
| path.join(outputDir, "project-1___base.json"), | ||
| "text-1", | ||
| "<p>Rich <strong>HTML</strong> content</p>" | ||
| ); | ||
| }); | ||
|
|
||
| it("should use plain text when richText is disabled at output level", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| const mockTextItem = createMockTextItem(); | ||
| setupMocks([mockTextItem], []); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }], | ||
| richText: "html", | ||
| outputs: [{ format: "json", outDir: outputDir, richText: false }], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify plain text content was written despite base config | ||
| assertFileContainsText( | ||
| path.join(outputDir, "project-1___base.json"), | ||
| "text-1", | ||
| "Plain text content" | ||
| ); | ||
| }); | ||
|
|
||
| it("should use rich text when enabled only at output level", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| const mockTextItem = createMockTextItem(); | ||
| setupMocks([mockTextItem], []); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }], | ||
| outputs: [{ format: "json", outDir: outputDir, richText: "html" }], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify rich text content was written | ||
| assertFileContainsText( | ||
| path.join(outputDir, "project-1___base.json"), | ||
| "text-1", | ||
| "<p>Rich <strong>HTML</strong> content</p>" | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Filter Feature", () => { | ||
| it("should filter projects when configured at base level", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }, { id: "project-2" }], | ||
| outputs: [ | ||
| { | ||
| format: "json", | ||
| outDir: outputDir, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify correct API call with filtered params | ||
| expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { | ||
| params: { | ||
| filter: '{"projects":[{"id":"project-1"},{"id":"project-2"}]}', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("should filter variants at base level", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }], | ||
| variants: [{ id: "variant-a" }, { id: "variant-b" }], | ||
| outputs: [ | ||
| { | ||
| format: "json", | ||
| outDir: outputDir, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify correct API call with filtered params | ||
| expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { | ||
| params: { | ||
| filter: | ||
| '{"projects":[{"id":"project-1"}],"variants":[{"id":"variant-a"},{"id":"variant-b"}]}', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("should filter projects at output level", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }, { id: "project-2" }], | ||
| outputs: [ | ||
| { | ||
| format: "json", | ||
| outDir: outputDir, | ||
| projects: [{ id: "project-1" }], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify correct API call with filtered params | ||
| expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { | ||
| params: { | ||
| filter: '{"projects":[{"id":"project-1"}]}', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("should filter variants at output level", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| projects: [{ id: "project-1" }], | ||
| variants: [{ id: "variant-a" }, { id: "variant-b" }], | ||
| outputs: [ | ||
| { | ||
| format: "json", | ||
| outDir: outputDir, | ||
| variants: [{ id: "variant-a" }], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify correct API call with filtered params | ||
| expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { | ||
| params: { | ||
| filter: | ||
| '{"projects":[{"id":"project-1"}],"variants":[{"id":"variant-a"}]}', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("supports the default filter behavior", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| appContext.setProjectConfig({ | ||
| outputs: [ | ||
| { | ||
| format: "json", | ||
| outDir: outputDir, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify correct API call with filtered params | ||
| expect(mockHttpClient.get).toHaveBeenCalledWith("/v2/textItems", { | ||
| params: { | ||
| filter: "{}", | ||
| }, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Output files", () => { | ||
| it("should create output files for each project and variant returned from the API", async () => { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| // project-1 and project-2 each have at least one base text item | ||
| const baseTextItems = [ | ||
| createMockTextItem({ | ||
| projectId: "project-1", | ||
| variantId: null, | ||
| id: "text-1", | ||
| }), | ||
| createMockTextItem({ | ||
| projectId: "project-1", | ||
| variantId: null, | ||
| id: "text-2", | ||
| }), | ||
| createMockTextItem({ | ||
| projectId: "project-2", | ||
| variantId: null, | ||
| id: "text-3", | ||
| }), | ||
| ]; | ||
|
|
||
| // project-1 and project-2 each have a variant-a text item | ||
| const variantATextItems = [ | ||
| createMockTextItem({ | ||
| projectId: "project-1", | ||
| variantId: "variant-a", | ||
| id: "text-4", | ||
| }), | ||
| createMockTextItem({ | ||
| projectId: "project-2", | ||
| variantId: "variant-a", | ||
| id: "text-5", | ||
| }), | ||
| ]; | ||
|
|
||
| // Only project-1 has variant-b, so only project-1 should get a variant-b file | ||
| const variantBTextItems = [ | ||
| createMockTextItem({ | ||
| projectId: "project-1", | ||
| variantId: "variant-b", | ||
| id: "text-6", | ||
| }), | ||
| createMockTextItem({ | ||
| projectId: "project-1", | ||
| variantId: "variant-b", | ||
| id: "text-7", | ||
| }), | ||
| ]; | ||
|
|
||
| setupMocks( | ||
| [...baseTextItems, ...variantATextItems, ...variantBTextItems], | ||
| [] | ||
| ); | ||
|
|
||
| await pull(); | ||
|
|
||
| // Verify a file was created for each project and variant present in the (mocked) API response | ||
| assertFilesCreated(outputDir, [ | ||
| "project-1___base.json", | ||
| "project-1___variant-a.json", | ||
| "project-1___variant-b.json", | ||
| "project-2___base.json", | ||
| "project-2___variant-a.json", | ||
| "variables.json", | ||
| ]); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.