diff --git a/AGENTS.md b/AGENTS.md index c194d9d..235bf7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,13 @@ like `line-numbers` depend on. Without `prism-plugin`, code is rendered via `Pri `hljs.highlight` into `v-html`. Users import plugin JS/CSS themselves and add the plugin class (e.g. `class="line-numbers"`), which is passed through to the `
`. +## Attributes + +The component sets `inheritAttrs: false` and places attributes itself: `class` goes on both the +wrapper and the `` (wrapper styling plus Prism plugin classes), `style` on the wrapper only, and +everything else (`id`, `data-*`, `aria-*`, listeners) on the `` only. Keep `class` duplicated; +Prism plugins need it on the ``. + ## Build Process ```bash @@ -88,6 +95,18 @@ Tests live in `src/plugin/__tests__/`, `src/plugin/components/__tests__/`, and `src/plugin/composables/__tests__/`. Update snapshots with `pnpm vitest --run -u` only when the change is intended. +### Test in the playground + +Unit tests run in jsdom, which does not cover real rendering, theme CSS, or Prism plugins well. +After any change to the component's rendering, attributes, styles, themes, or plugins, also check +it in the browser playground at http://127.0.0.1:3000/vue-code-block/playground/ (served by +`pnpm dev` / `pnpm play`). + +- The playground page is `src/playground/PlaygroundPage.ts.vue`. It is gitignored scratch space + (created from `src/playground/configs/templates/` if missing), so edit it freely to set up the + case being tested, and restore it afterwards. +- Check the rendered DOM and the browser console, not just that the page loads. + ## Linting ```bash diff --git a/src/plugin/VCodeBlock.vue b/src/plugin/VCodeBlock.vue index fe2997f..f031aee 100644 --- a/src/plugin/VCodeBlock.vue +++ b/src/plugin/VCodeBlock.vue @@ -1,5 +1,8 @@ -+@@ -146,8 +149,12 @@ const prismThemesVersion = '1.9.0'; // -------------------------------------------------- Emits & Slots & Injects // +// Attributes are placed manually so they are not duplicated on the wrapper and the// +defineOptions({ inheritAttrs: false }); + const emit = defineEmits(['run', 'update:copy-status']); const slots = useSlots(); +const attrs = useAttrs(); // -------------------------------------------------- Props // @@ -189,6 +196,22 @@ const runTextValue = ref(''); const useTheme = ref (''); +// -------------------------------------------------- Attributes // +// class goes on both elements (wrapper styling + Prism plugin classes like line-numbers), // +// style goes on the wrapper, everything else (id, data-*, aria-*, listeners) goes on the // +// These are functions, not computed, because attrs is not reactive // +function wrapperAttrs(): Record{ + return { + class: attrs.class, + style: attrs.style, + }; +} + +function preAttrs(): Record { + return Object.fromEntries(Object.entries(attrs).filter(([key]) => key !== 'style')); +} + + // -------------------------------------------------- Computed // // Prism plugins (line-numbers, etc.) only run when Prism highlights the element itself // const usePrismPlugin = computed (() => !!(settings.value.prismjs && settings.value.prismPlugin)); diff --git a/src/plugin/__tests__/VCodeBlock.test.ts b/src/plugin/__tests__/VCodeBlock.test.ts index 33b5e4c..e46c67b 100644 --- a/src/plugin/__tests__/VCodeBlock.test.ts +++ b/src/plugin/__tests__/VCodeBlock.test.ts @@ -90,6 +90,65 @@ describe('VCodeBlock Component', () => { }); + // -------------------------------------------------- Attributes // + describe('Attributes', () => { + it('should put the class on both the wrapper and the pre', () => { + const wrapper = mount(VCodeBlock, { + attrs: { class: 'foo' }, + props: { highlightjs: true } + }); + + expect(wrapper.classes()).toContain('foo'); + expect(wrapper.classes()).toContain('v-code-block'); + expect(wrapper.find('pre').classes()).toContain('foo'); + expect(wrapper.find('pre').classes()).toContain('language-javascript'); + }); + + it('should put the style on the wrapper only', () => { + const wrapper = mount(VCodeBlock, { + attrs: { style: 'margin: 10px;' }, + props: { highlightjs: true } + }); + + expect(wrapper.attributes('style')).toContain('margin: 10px'); + expect(wrapper.find('pre').attributes('style')).not.toContain('margin'); + }); + + it('should put the id, data and aria attributes on the pre only', () => { + const wrapper = mount(VCodeBlock, { + attrs: { + 'aria-label': 'Example code', + 'data-line': '2', + id: 'example', + }, + props: { highlightjs: true } + }); + + const pre = wrapper.find('pre'); + + expect(wrapper.findAll('#example')).toHaveLength(1); + expect(pre.attributes('id')).toBe('example'); + expect(pre.attributes('data-line')).toBe('2'); + expect(pre.attributes('aria-label')).toBe('Example code'); + expect(wrapper.attributes('id')).toBeUndefined(); + expect(wrapper.attributes('data-line')).toBeUndefined(); + expect(wrapper.attributes('aria-label')).toBeUndefined(); + }); + + it('should only call a click listener once', async () => { + const onClick = vi.fn(); + const wrapper = mount(VCodeBlock, { + attrs: { onClick }, + props: { highlightjs: true } + }); + + await wrapper.find('pre').trigger('click'); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + }); + + // -------------------------------------------------- Prism Plugins // describe('Prism Plugins', () => { it('should render line numbers when using the prism line-numbers plugin', async () => {