Page MenuHomePhabricator

Implement Vue 3 Composition API in ext.wikilambda.app
Open, In Progress, MediumPublic

Description

Vue 3 Composition API Migration

Overview

Migrating WikiLambda Vue components from Options API to Composition API using the setup() function pattern.

IMPORTANT: Only Vue components need migration. Pinia stores work with both APIs - no conversion needed.

The Pattern

Hybrid single <script> Tag with setup() Function that still uses module.exports

vue
<script>
const { computed, defineComponent, inject, ref } = require( 'vue' );
const useZObject = require( '../../composables/useZObject.js' );
const useMainStore = require( '../../store/index.js' );

module.exports = exports = defineComponent( {
  name: 'my-component',
  components: {
    'child-component': ChildComponent
  },
  props: {
    keyPath: {
      type: String,
      required: true
    },
    objectValue: {
      type: Object,
      required: true
    }
  },
  emits: [ 'update-value' ],
  setup( props, { emit } ) {
    // 1. Inject i18n (provided by MediaWiki)
    const i18n = inject( 'i18n' );

    // 2. Use composables (destructure only what's needed)
    const { getZObjectType, getZBooleanValue } = useZObject();

    // 3. Access store (DO NOT destructure - use store.property)
    const store = useMainStore();

    // 4. Reactive state
    const localState = ref( 'initial' );

    // 5. Computed properties
    const displayValue = computed( () => getZObjectType( props.objectValue ) );

    // 6. Methods
    function handleUpdate( value ) {
      store.setValueByKeyPath( value );  // Use store.action()
      emit( 'update-value', value );
    }

    // 7. MUST explicitly return everything the template uses
    return {
      // Only return what template actually uses
      getZObjectType,
      displayValue,
      handleUpdate,
      // Expose store getters/actions used in template
      getLabelData: store.getLabelData
    };
  }
} );
</script>

Key Rules

DO:

  • Use single <script> tag
  • Use setup( props, { emit } ) inside defineComponent()
  • Access props via props.propName
  • Use emit() for events
  • Use inject( 'i18n' ) for internationalization (MediaWiki provides i18n via app.provide)
  • Access store via store.property (e.g., store.getLabelData, store.setValueByKeyPath())
  • Use storeToRefs() ONLY for getters returned by reference in the return statement
  • Explicitly return all values/methods template needs
  • Destructure only needed methods from composables

DON'T:

  • Use two <script> tags
  • Use spread operators in returns
  • Use this (use props/emit instead)
  • Use getCurrentInstance() for i18n (use inject( 'i18n' ) instead)
  • Destructure from store (can cause reactivity issues and test complications)
  • Return everything (only what template needs)

Pinia Store Access

When to Use storeToRefs()

Per Pinia docs, use storeToRefs() to destructure state and getters while maintaining reactivity:

setup( props, { emit } ) {
  const store = useMainStore();

  // ✅ CORRECT: Use storeToRefs when destructuring getters/state
  const { isInitialized, getCurrentView } = storeToRefs( store );

  // ✅ CORRECT: Actions can be destructured directly (they're just functions)
  const { setValueByKeyPath, fetchZids } = store;

  // ✅ CORRECT: Access store directly in computed/methods
  const displayValue = computed( () => store.getLabelData( someId ) );

  function handleClick() {
    setValueByKeyPath( value );  // ✅ Destructured action
    store.fetchZids( zids );     // ✅ Or call directly
  }

  return {
    // Reactive refs from storeToRefs
    isInitialized,
    getCurrentView,
    // Other data
    displayValue,
    handleClick
  };
}

The Rules

  1. Destructure getters/state with storeToRefs() - Maintains reactivity
  2. Destructure actions directly from store - They're just functions, no reactivity needed
  3. Or access everything via store.property - Simpler, always works

Why? Pinia getters and state are reactive refs. Direct destructuring without storeToRefs() breaks reactivity. Actions are plain functions and can be destructured directly.

Composables Available

ComposableReplacesMethods Provided
useZObject()zobjectMixingetZObjectType, getZBooleanValue, etc.
useType()typeMixintypeToString, isValidZidFormat, etc.
useError(options)errorMixinfieldErrors, hasFieldErrors, clearFieldErrors
useEventLog()eventLogMixinlogEvent
usePageTitle(options)pageTitleMixinupdatePageTitle
useMetadata()metadataMixinmetadata utils
useClipboard(options)clipboardMixincopy, getCopiedText

Note: Some composables accept optional parameters:

  • useError({ keyPath }) - optional keyPath for error tracking
  • useZObject({ keyPath }) - optional keyPath for context

Migration Steps

  1. Remove mixin imports
  2. Import corresponding composables
  3. Add setup( props, { emit } ) function
  4. Convert data() to ref() or reactive()
  5. Convert computed to computed()
  6. Convert methods to functions
  7. Return all values/methods template uses
  8. Test the component


Migration Status

🏆 MIGRATION COMPLETE: 84 / 84 Files (100%) 🏆

Every Vue component and view has been successfully migrated to Composition API!

✅ Completed: Components with Mixins (40/40)

✅ Completed: Components without Mixins (40/40)

✅ Completed: View Files (4/4)

Composables Created (9/9)

Pinia Stores

NO MIGRATION NEEDED

Stores work with both Options and Composition API. Access via const store = useMainStore();

Key Patterns Established

Store Access

// Get store instance
const store = useMainStore();

// Use storeToRefs for getters you're returning by reference
const { isInitialized, getCurrentView } = storeToRefs( store );

// Access store directly in computed/methods
const value = computed( () => store.getLabelData( id ) );
store.setValueByKeyPath( data );

// Return with organized sections
return {
  // Reactive store data
  isInitialized,
  getCurrentView,
  // Other data
  value,
  handleClick
};

i18n Access

// Inject once
const i18n = inject( 'i18n' );

// Return for template use
return { i18n };

// Use in template
{{ i18n( 'message-key' ).text() }}

Test Refactoring Status

🔄 IN PROGRESS: Test Behavior Migration

Goal: Refactor tests from implementation testing to behavior testing after Composition API migration.

Status: test files need refactoring

See: TEST_REFACTORING_TODOS.md for detailed refactoring plans and progress tracking

Migration Results

  • 84 files converted (80 components + 4 views)
  • 9 composables created to replace 8 mixins
  • 0 linting errors
  • All tests passing
  • Full JSDoc documentation preserved
  • Proper reactivity maintained with storeToRefs pattern
  • 🔄 Test refactoring in progress

Test Refactoring TODOs - Behavior Testing Migration

This document outlines the test files that need refactoring from implementation testing to behavior testing after the Vue 3 Composition API migration.

🎯 Overview

During the migration from Options API to Composition API, many tests were updated but still use implementation testing patterns (accessing wrapper.vm properties, internal component state, etc.). This document prioritizes which tests need refactoring to focus on user behavior instead of implementation details.

📊 Current Status

  • 10 files still using wrapper.vm (implementation testing) - 9 completed
  • 1 file still using $nextTick (need waitFor migration) - 5 completed
  • 72 files with excessive find usage (1009+ calls)
  • 6 files need render wrapper functions (DRY improvement) - 16 completed
  • 44 files already have render wrappers ✅
  • 12 files render wrapper pattern fixed (simplified stubs handling) ✅

🚀 Systematic Cleanup Tasks

1) 🔧 Behavior Testing Migration

Replace wrapper.vm access with user behavior testing:

2) ⏱️ Async Testing Migration

Replace $nextTick with waitFor() for better async handling:

3) 🧹 DRY Improvement

Create render wrapper functions to eliminate repetitive setup code:

📋 Quick Reference

Testing Best Practices

  • Focus on user behavior over implementation details
  • Use waitFor() for asynchronous assertions
  • Prefer mount() over shallowMount() when testing user interactions
  • Test component integration, not just unit functionality
  • Ensure tests are maintainable and readable

Rules

  • find vs get: Use get() when element must exist (test fails if not found), use find() when element might not exist
  • wrapper.vm: Avoid accessing internal component state, test user-observable behavior instead
  • $nextTick: Replace with waitFor() for better async handling
  • Render wrappers: Create DRY helper functions to eliminate repetitive setup code
  • Render wrapper pattern: Use simplified stubs handling - { stubs: { Component: false } } instead of complex global object spreading

Details

Event Timeline

Change #1194606 had a related patch set uploaded (by Daphne Smit; author: Daphne Smit):

[mediawiki/extensions/WikiLambda@master] Part #1 - Composition API Migration - components and composables

https://gerrit.wikimedia.org/r/1194606

DSmit-WMF updated the task description. (Show Details)

Patch is up. Some leftover improvement recommendations and future work:

Cleanup

1. Improve Watch Statements

Issue: Check for any remaining invalid watch sources that might cause Vue warnings.

Recommendation: Audit all watch() statements to ensure they use proper reactive references.

2. Standardize Render Wrapper Patterns

Issue: Ensure all test files use consistent and nice render wrapper patterns with proper stub merging.

3. Check reactivity is working

Issue: because we now have storeToRefs and don't put getters with function in it. but that should work automagically. need to test that well.
Also need to check if all functions are imported from the correct place, eg; from correct composable or the store.

4. Check lost or missing JSDOC Comments

Recommendation: Add comprehensive JSDoc comments to composables and complex components.


Future work

1. Add Missing Test Coverage after Migration

Recommendation: Check for components that might be missing comprehensive test coverage.

2. Type Definitions

Recommendation: Consider adding TypeScript definitions for better developer experience.


Test Suite Improvements

1. Remove wrapper.vm. Usage (High Priority)

Issue: 57 instances across 11 files still use wrapper.vm. which tests implementation details instead of user behavior.

2. Replace leftover $nextTick with waitFor from testing-library

3. Optimize find() vs get() Usage

Issue: 477 instances of find() calls, many could be get() when elements are expected to exist.

Examples:

// ❌ When element should exist
const button = wrapper.find('button[data-testid="submit"]');
expect(button.exists()).toBe(true);

// ✅ Better
const button = wrapper.get('button[data-testid="submit"]'); // will throw when does not exist
DSantamaria changed the task status from Open to In Progress.Wed, Nov 12, 7:00 AM

Change #1194606 merged by jenkins-bot:

[mediawiki/extensions/WikiLambda@master] Vue 3 refactoring: Composition API Migration

https://gerrit.wikimedia.org/r/1194606