Convert hints editor unit tests to Vue Testing Library#5842
Convert hints editor unit tests to Vue Testing Library#5842Swoyamjeetcodes wants to merge 35 commits intolearningequality:unstablefrom
Conversation
|
👋 Hi @Swoyamjeetcodes, thanks for contributing! For the review process to begin, please verify that the following is satisfied:
Also check that issue requirements are satisfied & you ran Pull requests that don't follow the guidelines will be closed. Reviewer assignment can take up to 2 weeks. |
|
📢✨ Before we assign a reviewer, we'll turn on |
rtibblesbot
left a comment
There was a problem hiding this comment.
Clean migration — all acceptance criteria from #5815 met.
CI passing. Phase 3 skipped (no UI files changed). Two of the three PR videos (before/after recordings) failed to download — their S3 pre-signed URLs appear to have expired; only the test results video is accessible.
- praise: see inline — thorough open-index tracking coverage
- nitpick: see inline —
toBeDefined()inclickToolbarAction
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Reviewed the pull request diff checking for:
- Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
- Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
- Architecture: duplicated concerns, minimal interfaces, composition over inheritance
- Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
- Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
- Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
- Checked CI status and linked issue acceptance criteria
- For UI changes: inspected screenshots for layout, visual completeness, and consistency
| .trigger('click'); | ||
| const clickToolbarAction = async ({ action, hintIdx, user }) => { | ||
| const buttons = screen.getAllByTestId(`toolbarIcon-${action}`); | ||
| expect(buttons[hintIdx]).toBeDefined(); |
There was a problem hiding this comment.
nitpick: toBeDefined() is a plain Jest matcher. Since buttons[hintIdx] is a DOM node, toBeInTheDocument() is more idiomatic Testing Library and produces a clearer failure message if the button isn't found at the expected index.
|
📢✨ Before we assign a reviewer, we'll invite community pre-review. See the community review guidance for both authors and reviewers. |
AlexVelezLl
left a comment
There was a problem hiding this comment.
Thanks a lot for your contribution @Swoyamjeetcodes! Just a couple of things we may want to change to better align with new testing conventions in the org. Thanks!
| configure({ | ||
| testIdAttribute: 'data-test', | ||
| }); |
There was a problem hiding this comment.
@MisRob, I know we agreed on using data-testid for this, but I see that if we want to change this here, this will create a domino effect because there are tests relying on components like AssessmentItemToolbar that are also tested on other test suites, and we may end up modifying too many files for this. Is it okay if we leave it like this for now, and then, when all these related components are migrated, make the change to data-testid?
| expect(screen.getByRole('button', { name: 'New hint' })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders a placeholder when there are no hints', () => { | ||
| wrapper = mount(HintsEditor, { | ||
| propsData: { | ||
| hints: [], | ||
| }, | ||
| it('shows an empty-state message when a question has no hints', () => { | ||
| renderComponent({ | ||
| hints: [], | ||
| }); | ||
|
|
||
| expect(wrapper.html()).toContain('Question has no hints'); | ||
| expect(screen.getByText('Question has no hints')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders all hints in a correct order', () => { | ||
| wrapper = mount(HintsEditor, { | ||
| propsData: { | ||
| hints: [ | ||
| { hint: 'First hint', order: 1 }, | ||
| { hint: 'Second hint', order: 2 }, | ||
| ], | ||
| }, | ||
| it('shows hints in the same order as the question', () => { | ||
| renderComponent({ | ||
| hints: [ | ||
| { hint: 'First hint', order: 1 }, | ||
| { hint: 'Second hint', order: 2 }, | ||
| ], | ||
| }); | ||
|
|
||
| // Find all instances of your new RichTextEditor component | ||
| const editors = wrapper.findAllComponents({ name: 'RichTextEditor' }); | ||
| expect(editors.length).toBe(2); | ||
|
|
||
| // Instead of checking the raw HTML, we check the `value` prop passed to each editor. | ||
| expect(editors.at(0).props('value')).toBe('First hint'); | ||
| expect(editors.at(1).props('value')).toBe('Second hint'); | ||
| const hintCards = getHintCards(); | ||
| expect(within(hintCards[0]).getByText('First hint')).toBeInTheDocument(); | ||
| expect(within(hintCards[1]).getByText('Second hint')).toBeInTheDocument(); |
There was a problem hiding this comment.
Could we use the translation keys instead of the hardcoded strings for labels, please? So, instead of
screen.getByRole('button', { name: 'New hint' })We could do
screen.getByRole('button', { name: HintsEditor.$trs.newHintBtnLabel })| const MockTipTapEditor = { | ||
| name: 'TipTapEditor', | ||
| props: { | ||
| value: { | ||
| type: String, | ||
| default: '', | ||
| }, | ||
| mode: { | ||
| type: String, | ||
| default: 'view', | ||
| }, | ||
| }, | ||
| template: ` | ||
| <div> | ||
| <p v-if="value">{{ value }}</p> | ||
| <button | ||
| v-if="mode === 'edit'" | ||
| type="button" | ||
| aria-label="Update hint text" | ||
| @click="$emit('update', 'Updated hint')" | ||
| > | ||
| Update hint text | ||
| </button> | ||
| </div> | ||
| `, |
There was a problem hiding this comment.
I imagine this would be useful for other tests, too. Could you please create the mock on contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/__mocks__/TipTapEditor.vue and then just mock it with jest.mock('shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue');
For this mock's implementation, let's do it in a way that's closer to the actual implementation, like having this expose only a textarea element that emits the update event on change. Then we can just query this textarea, and use user.type to get the update events :). This way, we don't need to create elements like this button that may be confusing.
|
Hi @Swoyamjeetcodes! Could you please take a look at the feedback provided by @AlexVelezLl? Also, do let us know if you are unable to. Thank you! |
Bumps [celery](https://github.com/celery/celery) from 5.6.0 to 5.6.3. - [Release notes](https://github.com/celery/celery/releases) - [Changelog](https://github.com/celery/celery/blob/v5.6.3/Changelog.rst) - [Commits](celery/celery@v5.6.0...v5.6.3) --- updated-dependencies: - dependency-name: celery dependency-version: 5.6.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](docker/setup-qemu-action@v3...v4) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps the babel group with 1 update: [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env). Updates `@babel/preset-env` from 7.29.0 to 7.29.2 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.2/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-version: 7.29.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: babel ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](axios/axios@v1.13.5...v1.15.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [workbox-precaching](https://github.com/googlechrome/workbox) from 7.3.0 to 7.4.0. - [Release notes](https://github.com/googlechrome/workbox/releases) - [Commits](GoogleChrome/workbox@v7.3.0...v7.4.0) --- updated-dependencies: - dependency-name: workbox-precaching dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
…earningequality#5841) * feat: populate draft ChannelVersion metadata during draft publishes fill_published_fields now accepts an optional draft_channel_version parameter. When provided, ChannelVersion-level fields are written to the draft object while channel-level fields (total_resource_count, published_size, published_data, version_info) are left untouched. mark_channel_version_as_distributable is also skipped for draft publishes. publish_channel now calls fill_published_fields in the draft branch, passing the draft ChannelVersion returned by create_draft_channel_version. Closes learningequality#5839 * refactor: simplify draft publish metadata implementation after review * fix: use queryset update to bypass ChannelVersion full_clean validation ChannelVersion.save() always calls full_clean(), which validates choices on ArrayField(IntegerField(choices=...)) for included_licenses. Custom license IDs used in existing tests (e.g. IDs 100, 101) are not in the standard choices list from le_utils, so save() raised ValidationError. Replace version_obj.save() with a queryset .update() to write metadata fields directly without triggering model-level validation. The data originates from the DB so validation is unnecessary, and M2M operations (special_permissions_included) continue to use the model instance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: address review feedback on draft publish tests and publish_channel structure - Merge redundant 'if not is_draft_version:' block into the 'else:' branch of the is_draft_version conditional in publish_channel, as requested by reviewer. - Refactor FillPublishedFieldsDraftTestCase into DraftPublishChannelTestCase which tests the complete publish_channel flow (with save_export_database mocked) rather than calling fill_published_fields directly. Tests now use a Special Permissions license node with published=True to exercise the special_permissions_included logic. - test_second_draft_publish_replaces_special_permissions_included now makes two real publish_channel calls with different license_description values and verifies the M2M is replaced (not accumulated) between calls. - test_mark_channel_version_as_distributable_not_called replaced by test_special_permissions_distributable_false_for_draft_publish which asserts the distributable field stays False on the resulting AuditedSpecialPermissionsLicense objects after a draft publish of a public channel, rather than mocking the method. * fix: replace channel.included_languages on publish instead of accumulating Use .set() instead of .add() so languages removed from a channel are cleared on subsequent publishes rather than accumulated indefinitely. Flagged by AlexVelezLl, confirmed as a bug by rtibbles. * fix: use get() instead of create() for Special Permissions license in tests loadconstants inserts licenses with explicit PKs, leaving the PK sequence at 1. Calling License.objects.create() in setUp() then collides with the existing row. Use get() to fetch the pre-existing license — the same pattern used in test_create_channel_versions.py and test_sync.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: move delete_public_channel_cache_keys into else branch of is_draft_version The previous commit claimed to have merged all 'if not is_draft_version' blocks into the else branch, but missed the delete_public_channel_cache_keys call. Move it inside the else block and simplify the condition to 'if channel.public'. * fix: revert queryset update to version_obj.save() for ChannelVersion metadata Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Loosen the snapshot condition in ChannelVersion.save() to also fire when version is None (draft rows), so every draft publish captures the current channel name, description, thumbnail encoding, tagline, and language on the draft ChannelVersion. Repeated draft publishes always reflect the latest channel state. Tests cover: first draft publish populates all four snapshot fields, and a second draft publish after mutating channel metadata (including language change from "en" to "fr") refreshes all four fields on the existing row. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Propagate is_draft_version from create_content_database into map_channel_to_kolibri_channel so the exported content DB records version=0 for draft publishes instead of channel.version + 1. Kolibri's version-upgrade logic replaces any DB whose version is lower than the incoming version, so version=0 ensures any real publish (version >= 1) will always supersede a draft. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This includes: - Updated translation files (.po and .json) - Compiled Django messages (.mo files) - Updated frontend i18n files
…ging draft publishing.
This includes: - Updated translation files (.po and .json) - Compiled Django messages (.mo files) - Updated frontend i18n files
…ri to avoid issues with autoformatting of downloaded i18n files.
…earningequality#5860) * Remap Unit extra_fields.options node IDs when copying via copy_node When a Unit topic (UNIT modality) is cloned, lesson node IDs and pre/post test node IDs in extra_fields.options are rewritten to the IDs of their copies. Entries whose source nodes were not part of the copy operation (e.g. excluded via excluded_descendants) are dropped. Adds _remap_unit_options() static helper on CustomContentNodeTreeManager, calls it in _recurse_to_create_tree() for the deep-copy path and in _copy() for the shallow-copy path. _deep_copy() and _copy() now return (nodes, source_copy_id_map) so the map is available to callers; copy_node() unwraps the tuple and returns only the node list. * Test Unit extra_fields.options remapping across copy paths Adds UnitCopyExtraFieldsTestCase covering: - lesson_objectives keys remapped to cloned lesson PKs (deep and shallow) - pre_test/post_test values remapped to cloned node PKs (deep and shallow) - excluded lesson entry dropped from cloned Unit's lesson_objectives - excluded Unit: copy succeeds with no remap error (deep and shallow) - resource excluded inside lesson: lesson entry preserved (deep and shallow) - assessment_objectives, learning_objectives, completion_criteria unchanged - standalone Unit copy remaps its lesson children correctly - extra_fields on Course and Lesson topics is copied verbatim * Remove pre/post test remapping — Unit options contain no such node-ID keys The pre_test/post_test entries in a Unit's options are assessment items on the Unit node itself, not separate ContentNode IDs. Remove the dead code in _remap_unit_options() and the fantasy test coverage that accompanied it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: drop test_standalone_unit_copy_remaps_lesson_children Reviewer noted this test is redundant — the same behaviour is already exercised by the deep/shallow copy integration tests for Unit nodes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumps [ajv](https://github.com/ajv-validator/ajv) from 8.17.1 to 8.18.0. - [Release notes](https://github.com/ajv-validator/ajv/releases) - [Commits](ajv-validator/ajv@v8.17.1...v8.18.0) --- updated-dependencies: - dependency-name: ajv dependency-version: 8.18.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 5 to 6.0.3. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](pnpm/action-setup@v5...v6.0.3) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.11.1 to 4.12.0. - [Release notes](https://github.com/pytest-dev/pytest-django/releases) - [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst) - [Commits](pytest-dev/pytest-django@v4.11.1...v4.12.0) --- updated-dependencies: - dependency-name: pytest-django dependency-version: 4.12.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
VAutocomplete eagerly evaluates getText(internalValue) as the fallback argument to getValue, even when the fallback is never used. In multiple mode (used in SearchFilters), internalValue is an Array of selected ids. Arrays are objects in JS, so getPropertyFromItem does not short-circuit on the primitive guard and calls languageText with the array directly. Since arrays have no native_name property, this throws a TypeError. Guard languageText against item being null/undefined or lacking a native_name (covers arrays, partial objects, and any future edge cases). Fixes learningequality#5740. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rtibblesbot
left a comment
There was a problem hiding this comment.
Prior findings
Unaddressed (re-raised below):
toBeDefined()vstoBeInTheDocument()inclickToolbarAction(nitpick) — no reply, code unchanged atHintsEditor.spec.js:57
0/1 prior findings resolved. 1 re-raised below.
CI has two failures — Python unit tests and Build all message files — both failing at "Install python dependencies with uv". The merge commit (90cf90d) left unresolved conflict markers in requirements.in and requirements.txt, which prevents uv from parsing those files. These files are also out of scope for this PR. Fix: rebase cleanly onto unstable, or resolve the conflicts by accepting the unstable side of both files.
No UI files changed — Phase 3 skipped.
Blocking: Unresolved merge conflicts in requirements.in and requirements.txt — see inline comments.
Nitpick (re-raised): toBeDefined() vs toBeInTheDocument() at HintsEditor.spec.js:57.
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Compared the current PR state against findings from a prior review:
- Retrieved prior bot reviews deterministically via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
- Checked CI status and linked issue acceptance criteria
| psycopg2-binary==2.9.11 | ||
| django-js-reverse==0.10.2 | ||
| django-registration==3.4 | ||
| <<<<<<< HEAD |
There was a problem hiding this comment.
blocking: This file has unresolved merge conflict markers committed by the merge commit (90cf90d). requirements.in is out of scope for this PR — it should not have been modified at all. Please rebase cleanly onto unstable (preferred), or resolve the conflict by keeping the unstable version of both le-utils and gunicorn and removing the conflict markers from this file and requirements.txt.
| latex2mathml==3.78.1 | ||
| # via -r requirements.in | ||
| <<<<<<< HEAD | ||
| le-utils==0.2.17 |
There was a problem hiding this comment.
blocking: Same unresolved merge conflict as requirements.in — conflict markers here prevent uv from parsing this file, causing the "Install python dependencies with uv" CI step to fail.
| .trigger('click'); | ||
| const clickToolbarAction = async ({ action, hintIdx, user }) => { | ||
| const buttons = screen.getAllByTestId(`toolbarIcon-${action}`); | ||
| expect(buttons[hintIdx]).toBeDefined(); |
There was a problem hiding this comment.
nitpick (re-raised from prior review): getAllByTestId already throws if no matching elements exist, so buttons[hintIdx] being present in the returned array is sufficient. Prefer expect(buttons[hintIdx]).toBeInTheDocument() — it's the idiomatic Testing Library assertion and produces a clearer failure message than toBeDefined().
Fixes #5815
Summary
Refactored
channelEdit/components/HintsEditor/HintsEditor.spec.jsto Vue Testing Library and rewrote the suite to reflect user interactions.What changed
@vue/test-utilsto:@testing-library/vue(render,screen,within)@testing-library/user-eventrenderComponenthelper (with router + component stubs).Manual verification
pnpm test contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.jsTest Suites: 1 passedTests: 12 passedUI evidence
Screen recording (Hints workflow in Questions tab)
Passed test cases
testcases.mp4
References
Reviewer guidance
contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.jspnpm test contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.jsAI usage
I used Codex (GPT-5) to help migrate the test suite and draft this PR description.
I critically reviewed and edited the generated output to match project testing conventions, removed unnecessary/legacy VTU patterns, and verified correctness by rerunning the migrated Jest file until all tests passed.