Skip to content
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

Unify all interactions with Clipboard & stub all Clipboard methods during tests #2365

Closed
wants to merge 17 commits into from

Conversation

VasylMarchuk
Copy link
Collaborator

@VasylMarchuk VasylMarchuk commented Oct 28, 2024

Brief

This unifies all interactions with the Clipboard by adding a new component CopyableAnything, and by using it in all components that previously interacted with navigator.clipboard manually (custom implementations in approximately 9 different places, each slightly different).

In addition, when running the tests suite, all navigator.clipboard.* API methods are stubbed with Sinon fakes before each test, and un-stubbed after, thus preventing any potential side-effects.

The timeout of the "Copied!" message shown after copying is also reduced from 1000 to 10 milliseconds in the test environment.

Details

  • Added CopyableAnything component:
    • @value value to copy to the clipboard
    • @onCopied callback to call after copying
    • @isDisabled when true, calling yielded performCopyTask does nothing
    • yields performCopyTask — call this to actually copy passed @value to clipboard
    • yields hasRecentlyCopied — will be true for a configured time after copying
    • tests included
  • Rewrote all components that used navigator.clipboard.writeText to use CopyableAnything component instead:
    • CopyableCode
    • CopyableTerminalCommand
    • ComparisonCard
    • EvaluationTab
    • HeaderContainer
    • RepositoryDropdown
    • ShareProgressModal
  • Added stubClipboard test helper:
    • runs before each test
    • no side-effects when calling navigator.clipboard.writeText in test environment
    • no need to check if (config.environment === 'test') in the code
  • Added copyConfirmationTimeout: 1000 to config, set it to 10 in in test environment
  • Extracted localStorage-stubbing logic into a test helper stubLocalStorage

Checklist:

  • I've thoroughly self-reviewed my changes
  • I've added tests for my changes, unless they affect admin-only areas (or are otherwise not worth testing)
  • I've verified any visual changes using Percy (add a commit with [percy] in the message to trigger)

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced the CopyableAnything component for enhanced clipboard functionality across various components.
    • Added a configuration property copyConfirmationTimeout for customizable copy feedback duration.
  • Bug Fixes

    • Improved user feedback for copy actions by dynamically updating button text based on copy status.
  • Refactor

    • Removed redundant copy handling methods from several components, centralizing functionality in CopyableAnything.
  • Tests

    • Added integration tests for the CopyableAnything component to validate copy functionality and state management.
    • Introduced stubbing functions for clipboard and local storage to streamline testing processes.

@VasylMarchuk VasylMarchuk added bug Something isn't working enhancement New feature or request labels Oct 28, 2024
@VasylMarchuk VasylMarchuk self-assigned this Oct 28, 2024
Copy link
Contributor

coderabbitai bot commented Oct 28, 2024

Walkthrough

The changes introduced in this pull request primarily focus on implementing a new CopyableAnything component across various parts of the application. This component centralizes the functionality for copying text to the clipboard, replacing previous implementations in multiple components. The modifications include updating templates and TypeScript files to utilize the new component, enhancing the user experience with dynamic feedback on copy actions. Additionally, a new configuration property for managing copy confirmation timeouts has been added.

Changes

File Path Change Summary
app/components/copyable-anything.hbs Added a line utilizing the yield helper to manage copy task and recent copy state.
app/components/copyable-anything.ts Introduced CopyableAnythingComponent with functionality for copying text, including properties and methods for managing copy state and callbacks.
app/components/copyable-code.hbs Replaced button with CopyableAnything component for handling copy functionality.
app/components/copyable-code.ts Removed implementation of CopyableCodeComponent, updated method signature for onCopyButtonClick.
app/components/copyable-terminal-command.hbs Replaced copy button with CopyableAnything, managing copy state through the new component.
app/components/copyable-terminal-command.ts Removed wasCopiedRecently property and associated logic.
app/components/course-admin/code-example-page/comparison-card.hbs Updated to use CopyableAnything for "Copy ID" functionality.
app/components/course-admin/code-example-page/comparison-card.ts Renamed class from ComparisonCard to ComparisonCardComponent, removed copy action method.
app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.hbs Replaced button with CopyableAnything for copying logs file contents.
app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.ts Removed handleCopyToClipboardButtonClick and wasRecentlyCopied property.
app/components/course-admin/submissions-page/submission-details/header-container.hbs Updated to use CopyableAnything for copying repository URL, commit SHA, and tree SHA.
app/components/course-admin/submissions-page/submission-details/header-container.ts Removed copy action methods for commit SHA, repository URL, and tree SHA.
app/components/course-page/repository-dropdown.hbs Replaced <div> with CopyableAnything for copying Git repository URL.
app/components/course-page/repository-dropdown.ts Renamed class from CoursePageRepositoryDropdownComponent to RepositoryDropdownComponent, removed copy action method.
app/components/course-page/share-progress-modal.hbs Replaced button with CopyableAnything for sharing progress.
app/components/course-page/share-progress-modal.ts Removed copyableText and wasCopiedRecently properties, simplified copy logic.
app/config/environment.d.ts Added copyConfirmationTimeout property to configuration.
app/models/repository.ts Introduced isNewAsBoolean getter in RepositoryModel.
config/environment.js Added copyConfirmationTimeout with default values for different environments.
tests/integration/components/copyable-anything-test.ts Introduced integration tests for CopyableAnything component functionality.
tests/support/stub-clipboard.ts Added function to stub clipboard methods for testing.
tests/support/stub-local-storage.ts Added function to stub local storage methods for testing.
tests/test-helper.js Updated to use new stubbing functions for local storage and clipboard.

Possibly related PRs

Suggested labels

dependencies

Suggested reviewers

  • rohitpaulk

Poem

🐇 In the land of code so bright,
A new component takes its flight.
Copy tasks now wrapped with care,
Dynamic states, a joy to share!
With each click, a message clear,
"Copied!" sings, bring us cheer! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

github-actions bot commented Oct 28, 2024

Test Results

  1 files  ±0    1 suites  ±0   6m 28s ⏱️ +11s
586 tests +4  546 ✅ +4  40 💤 ±0  0 ❌ ±0 
601 runs  +4  561 ✅ +4  40 💤 ±0  0 ❌ ±0 

Results for commit 2d1c80e. ± Comparison against base commit 1d8e0fa.

♻️ This comment has been updated with latest results.

Copy link

codecov bot commented Oct 28, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

✅ All tests successful. No failed tests found.

Additional details and impacted files

📢 Thoughts on this report? Let us know!

Copy link

codecov bot commented Oct 28, 2024

Bundle Report

Changes will decrease total bundle size by 1.57kB (-0.0%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
client-array-push 35.83MB 1.57kB (-0.0%) ⬇️

@VasylMarchuk VasylMarchuk force-pushed the stub-clipboard-during-tests branch 2 times, most recently from 0824b63 to 89a48b7 Compare October 28, 2024 15:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (17)
app/config/environment.d.ts (1)

14-14: Add JSDoc documentation for the new configuration property.

The addition of copyConfirmationTimeout is well-typed and correctly placed. Consider adding JSDoc documentation to describe its purpose, expected values, and units (milliseconds).

   x: {
     backendUrl: string;
+    /** Duration in milliseconds to show the copy confirmation message. Default: 1000ms */
     copyConfirmationTimeout: number;
     isCI: boolean;
app/components/copyable-code.hbs (1)

12-21: Enhance accessibility of the copy button.

The integration of CopyableAnything looks good and aligns with the PR objective of unifying clipboard interactions. However, consider adding accessibility attributes to improve user experience.

Apply these improvements to the button:

 <button
   type="button"
   class="bg-teal-600 px-3 py-1 text-white flex items-center rounded-r rounded-l-none text-sm font-bold"
   {{on "click" performCopyTask}}
   data-test-copy-button
+  aria-label="Copy code to clipboard"
+  title="Copy code to clipboard"
 >
   {{if hasRecentlyCopied "COPIED" "COPY"}}
 </button>
app/components/copyable-terminal-command.ts (1)

Line range hint 1-41: Well-structured component refactoring

The component has been successfully simplified by delegating clipboard functionality to CopyableAnything while maintaining its core responsibility of command text preparation. This separation of concerns improves maintainability and reduces code duplication.

tests/support/stub-clipboard.ts (1)

5-15: Consider adding example usage to the documentation.

The documentation is thorough, but it could be even more helpful with an example showing how to use this helper in tests.

Add example usage to the JSDoc:

 * @param {ClipboardItem[]} [cache=new Array<ClipboardItem>()] Predefined contents of the fake clipboard cache
 * @returns {ClipboardItem[]} Fake clipboard cache
+ *
+ * @example
+ * ```typescript
+ * // In your test file
+ * const clipboardCache = stubClipboard();
+ * 
+ * // Pre-populate clipboard
+ * await navigator.clipboard.writeText('test content');
+ * 
+ * // Assert clipboard content
+ * expect(await navigator.clipboard.readText()).to.equal('test content');
+ * ```
 */
tests/support/stub-local-storage.ts (1)

1-14: Enhance documentation with usage example.

The documentation is thorough, but adding an example usage would make it more developer-friendly.

Add the following to the JSDoc:

 /**
  * Stubs the following `window.localStorage` methods with sinon fakes
  * and returns the Map used for storing the fake localStorage cache:
  * - `setItem`
  * - `removeItem`
  * - `clear`
  * - `key`
  * - `get length`
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage}
  * @param {Map<string, string>} [cache=new Map<string, string>()] Predefined contents of the fake localStorage cache
  * @returns {Map<string, string>} Fake localStorage cache
+ * @example
+ * // Basic usage
+ * const cache = stubLocalStorage();
+ * localStorage.setItem('key', 'value');
+ * assert.equal(localStorage.getItem('key'), 'value');
+ * assert.equal(cache.get('key'), 'value');
+ *
+ * // With pre-populated cache
+ * const initialCache = new Map([['existingKey', 'existingValue']]);
+ * stubLocalStorage(initialCache);
  */
app/components/copyable-anything.ts (1)

12-13: Consider adding runtime validation for nullable value.

The value argument is typed as potentially null, but it's directly converted to String on line 32 without validation. While TypeScript ensures type safety at compile time, consider adding runtime validation for a better developer experience.

 async copy(): Promise<void> {
   if (this.args.isDisabled) {
     return;
   }
+  if (this.args.value == null) {
+    console.warn('CopyableAnything: Attempted to copy null or undefined value');
+    return;
+  }
   await navigator.clipboard.writeText(String(this.args.value));
app/components/course-page/share-progress-modal.hbs (1)

27-44: LGTM! Consider adding ARIA attributes for better accessibility.

The implementation correctly uses the new CopyableAnything component and provides clear visual feedback. The animation and state management are well-implemented.

Consider adding aria-label to improve accessibility:

-      <PrimaryButton {{on "click" performCopyTask}} data-test-copy-button>
+      <PrimaryButton 
+        {{on "click" performCopyTask}} 
+        data-test-copy-button
+        aria-label={{if hasRecentlyCopied "Copied to clipboard" "Copy to clipboard"}}
+      >
app/components/course-page/share-progress-modal.ts (2)

Line range hint 33-41: Consider improving the copyableText implementation.

The implementation could benefit from some refinements:

  1. The URL and platform-specific text could be extracted as constants
  2. Consider handling the case where language is undefined

Here's a suggested improvement:

+ private readonly BASE_URL = 'https://app.codecrafters.io/courses';
+ private readonly PLATFORM_PREFIXES = {
+   twitter: '@codecraftersio',
+   default: 'CodeCrafters'
+ };

  get copyableText() {
+   const platformPrefix = this.selectedSocialPlatform === 'twitter' 
+     ? this.PLATFORM_PREFIXES.twitter 
+     : this.PLATFORM_PREFIXES.default;
+   const language = this.args.repository.language?.name ?? 'selected language';
+   const courseSlug = this.args.repository.course.slug;
+   
    return `I'm working on the ${platformPrefix} ${this.args.repository.course.name} challenge in ${language}.\n\n${this.BASE_URL}/${courseSlug}/overview`;
  }

Line range hint 1-65: Great job on centralizing clipboard functionality!

The refactoring aligns well with the PR's objective of unifying clipboard interactions. The component now has a cleaner separation of concerns:

  • UI state management (selected platform)
  • Text content generation (copyableText)
  • Analytics tracking

This makes the code more maintainable and testable.

app/components/course-admin/code-example-page/comparison-card.ts (2)

Line range hint 1-16: Consider adding JSDoc documentation.

While the TypeScript interfaces provide good type safety, adding JSDoc comments would improve code maintainability by documenting:

  • The component's purpose
  • The significance of each argument
  • Any important implementation details

Example:

/**
 * Renders a comparison card for displaying solution comparisons between two users.
 * 
 * @component
 * @example
 * <CourseAdmin::CodeExamplePage::ComparisonCard
 *   @comparison={{this.comparison}}
 *   @firstUser={{this.user}}
 * />
 */

Line range hint 20-77: Consider simplifying the user and solution getters.

The component has multiple getters for handling user and solution comparisons. Consider refactoring to reduce complexity:

private getUserSolution(targetUser: UserModel) {
  const isFirstSolution = this.args.comparison.firstSolution.user.id === targetUser.id;
  return isFirstSolution ? this.args.comparison.firstSolution : this.args.comparison.secondSolution;
}

get solutionForFirstUser() {
  return this.getUserSolution(this.firstUser);
}

get solutionForSecondUser() {
  return this.getUserSolution(this.secondUser);
}
app/components/copyable-terminal-command.hbs (1)

34-48: Enhance accessibility for screen readers.

Consider adding ARIA attributes to improve accessibility:

-      <TertiaryButton @size="extra-small" {{on "click" performCopyTask}}>
+      <TertiaryButton 
+        @size="extra-small" 
+        aria-label="Copy command to clipboard"
+        {{on "click" performCopyTask}}
+      >
         <AnimatedContainer>
           {{#animated-if hasRecentlyCopied use=this.transition duration=200}}
-            <div class="flex items-center gap-1">
+            <div class="flex items-center gap-1" role="status" aria-live="polite">
               {{svg-jar "clipboard-check" class="h-4 w-4 text-teal-500"}}
               <span class="text-xs text-teal-500">copied!</span>
             </div>
config/environment.js (1)

31-31: Consider organizing configuration properties and adding documentation.

While the default timeout value is reasonable, consider:

  1. Grouping related UI configuration properties together
  2. Adding JSDoc comments to document the purpose and expected value range
     x: {
       backendUrl: process.env.BACKEND_URL || 'https://test-backend.ngrok.io',
 
+      // UI Configuration
+      /** Duration (ms) to show "Copied!" confirmation message */
       copyConfirmationTimeout: 1000,
 
       defaultMetaTags: {
app/components/course-admin/code-example-page/comparison-card.hbs (1)

40-49: LGTM! Consider adding ARIA attributes for better accessibility.

The integration of CopyableAnything is well implemented and aligns perfectly with the PR's objective of unifying clipboard interactions.

Consider adding aria-label to improve screen reader experience:

 <button
   class="flex-shrink-0 border hover:bg-gray-100 hover:border-gray-300 transition-colors px-2 py-1.5 text-gray-700 font-bold text-xs rounded flex items-center"
   type="button"
+  aria-label={{if hasRecentlyCopied "ID copied to clipboard" "Copy ID to clipboard"}}
   {{on "click" performCopyTask}}
 >
app/components/course-page/repository-dropdown.hbs (2)

45-81: Enhance accessibility of the copy button implementation.

The implementation successfully integrates with the new CopyableAnything component and maintains the existing UX. However, consider these accessibility improvements:

  1. Replace the div with a semantic button element:
-<div
+<button
   class="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 flex group
     {{if @activeRepository.isNewAsBoolean 'text-gray-300 dark:text-gray-700'}}"
   role="button"
   {{on "click" performCopyTask}}
   data-test-dropdown-action
->
  1. Add aria-label to provide better context for screen readers:
 <button
+  aria-label="Copy Git Repository URL"
   class="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 flex group

77-79: Move tooltip outside the clickable area.

The tooltip is currently rendered inside the clickable area, which could interfere with the copy action. Consider moving it outside:

-{{#if @activeRepository.isNew}}
-  <EmberTooltip @text="Please select a language first" />
-{{/if}}
</div>
+{{#if @activeRepository.isNew}}
+  <EmberTooltip @text="Please select a language first" />
+{{/if}}
app/models/repository.ts (1)

147-149: Add documentation for the isNewAsBoolean getter.

While the implementation is correct, please add a JSDoc comment explaining:

  1. The purpose of this getter
  2. Its relationship to clipboard functionality
  3. The source of the isNew property (inherited from Model)
+  /**
+   * Converts the inherited `isNew` property to a boolean.
+   * Used by CopyableAnything component to control the disabled state of clipboard operations.
+   * 
+   * @returns {boolean} True if the repository is new, false otherwise
+   */
   get isNewAsBoolean() {
     return Boolean(this.isNew);
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 1d8e0fa and 2d1c80e.

📒 Files selected for processing (23)
  • app/components/copyable-anything.hbs (1 hunks)
  • app/components/copyable-anything.ts (1 hunks)
  • app/components/copyable-code.hbs (1 hunks)
  • app/components/copyable-code.ts (1 hunks)
  • app/components/copyable-terminal-command.hbs (1 hunks)
  • app/components/copyable-terminal-command.ts (1 hunks)
  • app/components/course-admin/code-example-page/comparison-card.hbs (1 hunks)
  • app/components/course-admin/code-example-page/comparison-card.ts (3 hunks)
  • app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.hbs (1 hunks)
  • app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.ts (1 hunks)
  • app/components/course-admin/submissions-page/submission-details/header-container.hbs (3 hunks)
  • app/components/course-admin/submissions-page/submission-details/header-container.ts (1 hunks)
  • app/components/course-page/repository-dropdown.hbs (1 hunks)
  • app/components/course-page/repository-dropdown.ts (3 hunks)
  • app/components/course-page/share-progress-modal.hbs (1 hunks)
  • app/components/course-page/share-progress-modal.ts (2 hunks)
  • app/config/environment.d.ts (1 hunks)
  • app/models/repository.ts (1 hunks)
  • config/environment.js (2 hunks)
  • tests/integration/components/copyable-anything-test.ts (1 hunks)
  • tests/support/stub-clipboard.ts (1 hunks)
  • tests/support/stub-local-storage.ts (1 hunks)
  • tests/test-helper.js (2 hunks)
🔇 Additional comments (36)
app/components/copyable-anything.hbs (1)

1-1: LGTM! Consider adding TypeScript template validation.

The implementation follows Ember.js best practices for component composition and async task handling. The yielded values align perfectly with the component's intended API.

Consider adding TypeScript template validation using <template> tags to ensure type safety:

+<template>
{{yield (fn (perform this.copyTask)) this.hasRecentlyCopied}}
+</template>

Let's verify that all components using CopyableAnything are properly passing block content:

app/components/copyable-code.ts (1)

13-13: Verify migration to CopyableAnything component.

The component implementation has been removed as part of centralizing clipboard logic. Let's verify the corresponding template is using CopyableAnything.

✅ Verification successful

Migration to CopyableAnything component is properly implemented

The template is correctly using the CopyableAnything component for handling clipboard operations, with proper bindings for @value and @onCopied attributes, and appropriate usage of the yielded performCopyTask and hasRecentlyCopied values.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the template is using CopyableAnything
# Expect: CopyableAnything usage in the corresponding HBS template

# Find and check the corresponding template
fd --extension hbs copyable-code --exec cat {}

Length of output: 987

app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.ts (2)

2-3: LGTM! Clean import organization.

The imports are now logically grouped and unnecessary imports have been removed, making the code more maintainable.


Line range hint 1-38: Verify template integration with CopyableAnything.

The clipboard-related code has been correctly removed from this component. However, we should verify that the corresponding template file properly implements the new CopyableAnything component.

✅ Verification successful

Template correctly uses CopyableAnything component and has no direct clipboard interactions

The verification confirms that:

  • The template file evaluation-tab.hbs properly implements the CopyableAnything component
  • No direct clipboard interactions (like navigator.clipboard or copyToClipboard) remain in the template
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the corresponding HBS template uses CopyableAnything

# Test: Check if the template exists and uses CopyableAnything
rg -g "*.hbs" "CopyableAnything" "app/components/course-admin/code-example-page/evaluation-card/"

# Test: Ensure no direct clipboard interactions remain
rg -g "*.hbs" "navigator\.clipboard|copyToClipboard" "app/components/course-admin/code-example-page/evaluation-card/"

Length of output: 501

app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.hbs (2)

3-7: Well-implemented clipboard interaction refactor!

The integration of CopyableAnything is clean and maintains all the necessary functionality:

  • Preserves the disabled state when no logs content is available
  • Provides user feedback through the button text
  • Maintains consistent UX with the existing button styling

3-7: Verify consistent clipboard implementation across components.

Let's ensure this implementation follows the same pattern as other refactored components.

✅ Verification successful

Based on the search results, I can now generate the final response:

Implementation aligns with established clipboard patterns

The CopyableAnything component usage in evaluation-tab.hbs follows the consistent pattern found across the codebase:

  • Uses the same @value binding pattern
  • Yields performCopyTask and hasRecentlyCopied consistently
  • Implements similar button UI with copy state feedback

Other instances like copyable-code.hbs, copyable-terminal-command.hbs, and share-progress-modal.hbs follow identical patterns, confirming this implementation is part of the unified clipboard interaction approach.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent CopyableAnything implementation pattern across components
# Expected: Similar implementation pattern with @value, performCopyTask, and hasRecentlyCopied

# Search for other CopyableAnything implementations
echo "Checking CopyableAnything usage pattern across components:"
rg -A 3 "@value=\{\{.*\}\}" --type hbs

# Verify removal of old clipboard-related code
echo -e "\nChecking for any remaining direct clipboard interactions:"
rg -i "clipboard" --type ts

Length of output: 21448

app/components/copyable-terminal-command.ts (2)

4-4: LGTM: Proper typing for DarkModeService

The explicit type import enhances type safety for the dark mode service.


11-11: LGTM: Enhanced type signature for async copy operations

The updated signature properly supports both synchronous and asynchronous copy operations, which is essential for clipboard interactions.

Let's verify that all implementations of this callback are handling the async nature correctly:

tests/support/stub-clipboard.ts (2)

1-3: LGTM! Clean import and constant declaration.

The import is appropriate, and the constant follows naming conventions.


16-34: Well-implemented clipboard stubbing with proper state management.

The implementation correctly maintains clipboard state and handles all clipboard methods appropriately. The use of splice for cache updates ensures proper array mutation, and the optional chaining in readText provides safe access to potentially undefined values.

tests/test-helper.js (2)

9-11: LGTM! Import changes align with PR objectives.

The new imports for stubLocalStorage and stubClipboard helpers, along with the switch to ember-exam for test running, are well-structured changes that support the PR's goal of unifying clipboard interactions and improving test infrastructure.


21-22: Verify stub initialization order.

The stubs are correctly placed in the QUnit.testStart hook to ensure fresh stubs for each test. However, let's verify that there are no order-dependent side effects between these stubs.

✅ Verification successful

No order dependency between localStorage and clipboard stubs

After examining the implementation of both stubs, I can confirm that they are completely independent of each other:

  • stubLocalStorage only interacts with window.localStorage and its own cache Map
  • stubClipboard only interacts with navigator.clipboard and its own cache Array
  • Neither stub references or depends on the other's functionality or state

The order of initialization in the QUnit.testStart hook is safe as implemented.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any order dependencies between localStorage and clipboard stubs
# by looking for cross-references in their implementations

echo "Checking stub implementations for dependencies..."

# Look for clipboard references in localStorage stub
rg -l "clipboard" "tests/support/stub-local-storage.ts"

# Look for localStorage references in clipboard stub
rg -l "localStorage" "tests/support/stub-clipboard.ts"

# Look for test files that might rely on stub order
rg -l "stubLocalStorage.*stubClipboard|stubClipboard.*stubLocalStorage" "tests"

Length of output: 295


Script:

#!/bin/bash
# Let's first check if these files exist and examine their content
fd "stub-local-storage.ts|stub-clipboard.ts" tests/

# If not found with .ts extension, try .js
fd "stub-local-storage.js|stub-clipboard.js" tests/

# Let's examine the actual test-helper.js content
cat tests/test-helper.js

# Look for any test files that use these stubs
rg "stubLocalStorage|stubClipboard" tests/ -A 3 -B 3

Length of output: 3801


Script:

#!/bin/bash
# Let's examine the content of both stub files to understand their implementations
cat tests/support/stub-local-storage.ts
cat tests/support/stub-clipboard.ts

# Let's also check if there are any test files that specifically test these stubs
# or their interaction
rg -l "describe.*stub.*Storage|describe.*stub.*Clipboard" tests/

Length of output: 2697

tests/support/stub-local-storage.ts (1)

1-45: Implementation aligns well with PR objectives.

The stubLocalStorage helper effectively complements the clipboard stubbing infrastructure mentioned in the PR objectives. The implementation is thorough and provides a solid foundation for testing components that interact with localStorage.

app/components/copyable-anything.ts (4)

22-24: LGTM! Clean component setup.

The component class is well-structured with proper typing and state tracking.


45-47: LGTM! Proper task configuration.

The ember-concurrency task is well-configured with keepLatest to handle rapid copy attempts gracefully.


50-54: LGTM! Proper Glint integration.

The component is correctly registered with Glint for proper template type checking.


1-54: Verify complete migration to CopyableAnything.

Let's ensure all direct clipboard.writeText usages are migrated to this new component.

✅ Verification successful

Let me verify the implementation details of the components using CopyableAnything to ensure they're using it correctly.


Based on the comprehensive review of the implementation, I can now provide the final response:

Migration to CopyableAnything component is complete and correct.

The verification shows that:

  • The only direct navigator.clipboard.writeText usage is within the CopyableAnything component itself
  • All clipboard operations across the codebase are using the CopyableAnything component correctly
  • The component is being used in various contexts with proper implementation:
    • Terminal commands
    • Code snippets
    • Repository URLs
    • Commit/Tree SHAs
    • Progress sharing
    • Evaluation logs
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining direct clipboard API usage
echo "Searching for direct clipboard API usage..."
rg -l "navigator\.clipboard\.writeText" --type ts --type hbs

# Search for CopyableAnything usage to verify adoption
echo -e "\nVerifying CopyableAnything adoption..."
rg -l "CopyableAnything" --type ts --type hbs

Length of output: 838


Script:

#!/bin/bash
# Check implementation of components using CopyableAnything
echo "Checking copyable-terminal-command implementation..."
cat app/components/copyable-terminal-command.hbs

echo -e "\nChecking copyable-code implementation..."
cat app/components/copyable-code.hbs

echo -e "\nChecking repository-dropdown implementation..."
cat app/components/course-page/repository-dropdown.hbs

echo -e "\nChecking share-progress-modal implementation..."
cat app/components/course-page/share-progress-modal.hbs

echo -e "\nChecking comparison-card implementation..."
cat app/components/course-admin/code-example-page/comparison-card.hbs

echo -e "\nChecking header-container implementation..."
cat app/components/course-admin/submissions-page/submission-details/header-container.hbs

echo -e "\nChecking evaluation-tab implementation..."
cat app/components/course-admin/code-example-page/evaluation-card/evaluation-tab.hbs

Length of output: 26823

app/components/course-page/share-progress-modal.hbs (2)

27-27: Verify test coverage for clipboard interactions.

Since this component handles clipboard operations, let's verify that the tests properly stub the clipboard API as mentioned in the PR objectives.

#!/bin/bash
# Search for test files related to this component
fd "share-progress-modal-test" "tests"

# Check if the tests use the stubClipboard helper
rg "stubClipboard" "tests" -A 5

30-30: Verify animation duration consistency across components.

The animation duration is set to 100ms here. Let's verify this is consistent with other CopyableAnything implementations.

✅ Verification successful

Let me try a different search pattern to find animation duration usage across components.


Animation duration is consistent with similar copy-related components

The duration of 100ms in share-progress-modal matches other copy-related animations in the codebase:

  • dark-mode-toggle-option uses 100ms
  • onboarding-survey-wizard/selectable-item uses 100ms
  • Only copyable-terminal-command uses a slightly longer duration of 200ms

The implementation appears to be consistent with the established patterns in the codebase, with most quick feedback animations using 100ms duration. Longer animations (300ms) are used for more complex transitions like expanding sections.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other animation duration settings in similar components
rg -A 2 "duration=" "app/components" --type hbs

Length of output: 1129


Script:

#!/bin/bash
# Search for animated-if with duration parameter
rg -A 2 "animated-if.*duration=" "app/components" --type hbs

# Also search for other animation duration settings
rg -A 2 "duration=" "app/components" --type js --type ts

Length of output: 2334

app/components/course-page/share-progress-modal.ts (2)

3-7: LGTM! Import statements are well-organized.

The imports are properly typed and follow best practices.


Line range hint 43-54: Verify CopyableAnything integration in the template.

The action handlers look good, but let's ensure proper integration with CopyableAnything.

✅ Verification successful

CopyableAnything integration verified and properly implemented

The template shows correct integration with CopyableAnything component:

  • Proper binding of @value={{this.copyableText}}
  • Correct handler connection via @onCopied={{this.handleCopyButtonClick}}
  • Appropriate usage of yielded properties performCopyTask and hasRecentlyCopied
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify CopyableAnything usage in the template
# Expected: The template should use CopyableAnything with proper bindings

rg -t hbs "CopyableAnything.*@value={{this.copyableText}}.*@onCopied={{this.handleCopyButtonClick}}" app/components/course-page/

Length of output: 327


Script:

#!/bin/bash
# Search for CopyableAnything usage in the template with proper escaping
rg -t hbs "CopyableAnything" app/components/course-page/ -A 3 -B 3

Length of output: 2505

app/components/course-admin/code-example-page/comparison-card.ts (2)

17-17: LGTM! Class renaming improves clarity.

The renaming from ComparisonCard to ComparisonCardComponent follows better naming conventions by being more explicit about the component's nature. The change is consistently applied in both the class declaration and Glint registry.

Also applies to: 81-81


Line range hint 1-81: Verify clipboard functionality migration.

The removal of clipboard-related code aligns with the PR's objective to unify clipboard interactions. Let's verify that all clipboard functionality has been properly migrated to the CopyableAnything component.

✅ Verification successful

Let me check the actual template content to verify the clipboard functionality migration.


Clipboard functionality has been properly migrated to CopyableAnything

The template shows that clipboard functionality has been correctly migrated to use the CopyableAnything component for copying the comparison ID. The only clipboard-related reference is to an icon (clipboard-check). No direct clipboard API usage remains in the component.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining direct clipboard API usage in the component's template

# Test: Search for direct clipboard API usage
rg -l "navigator\.clipboard" "app/components/course-admin/code-example-page/comparison-card"

# Test: Verify CopyableAnything is properly imported in the template
rg -l "CopyableAnything" "app/components/course-admin/code-example-page/comparison-card.hbs"

Length of output: 351


Script:

#!/bin/bash
# Check the template content to verify proper usage of CopyableAnything
cat app/components/course-admin/code-example-page/comparison-card.hbs

# Also search for any clipboard-related code in the component
rg -g "comparison-card.*" "clipboard" app/components/course-admin/code-example-page/

Length of output: 3235

app/components/copyable-terminal-command.hbs (1)

33-49: Implementation aligns well with PR objectives!

The integration of CopyableAnything maintains the existing UX while standardizing clipboard interactions.

Let's verify the copyableText implementation:

✅ Verification successful

Implementation is correct and well-implemented

The copyableText getter is properly implemented in app/components/copyable-terminal-command.ts:

get copyableText(): string {
  return this.args.commands.map((command) => command.replace(/# .*$/, '')).join('\n');
}

It correctly processes the commands by stripping comments and joining them with newlines, making it suitable for clipboard operations.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the implementation of copyableText in the component's TypeScript file
ast-grep --pattern 'copyableText = $$$'

Length of output: 41


Script:

#!/bin/bash
# Let's try to find the component's TypeScript/JavaScript file first
fd "copyable-terminal-command" --type f

# Then let's search for any getter or computed property definition for copyableText
ast-grep --pattern 'get copyableText() { $$$ }'

# Also search for class property definition
ast-grep --pattern 'copyableText = $$$'

# Backup search using ripgrep for any copyableText references
rg "copyableText" -A 3 -B 3

Length of output: 8761

app/components/course-admin/submissions-page/submission-details/header-container.ts (2)

4-4: LGTM! Modern service injection syntax.

The update to use import { service } instead of import { inject as service } aligns with current Ember.js best practices.


Line range hint 1-108: Verify template integration with CopyableAnything component.

The removal of clipboard-related methods suggests that the corresponding template file now uses the CopyableAnything component. Let's verify this integration:

✅ Verification successful

Let me check the actual template content to verify the integration properly.


CopyableAnything component is properly integrated

The template shows correct integration of the CopyableAnything component in multiple places:

  • Git clone URL copying
  • Commit SHA copying
  • Tree SHA copying

Each integration follows the correct pattern using the component's block form with performCopyTask and hasRecentlyCopied parameters. The clipboard icons (clipboard-list) are only used within the CopyableAnything context for visual feedback, not for direct clipboard operations.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the template file properly integrates with CopyableAnything
# and that no direct clipboard operations remain.

# Test 1: Check if the template uses CopyableAnything
echo "Checking for CopyableAnything usage in the template:"
rg -l "CopyableAnything" "app/components/course-admin/submissions-page/submission-details/header-container.hbs"

# Test 2: Verify no direct clipboard operations remain
echo "Checking for any remaining direct clipboard operations:"
rg -i "clipboard" "app/components/course-admin/submissions-page/submission-details/header-container.hbs"

Length of output: 783


Script:

#!/bin/bash
# Get the template content to verify CopyableAnything integration
echo "Template content:"
cat "app/components/course-admin/submissions-page/submission-details/header-container.hbs"

Length of output: 9645

config/environment.js (1)

88-88: Verify if 10ms is sufficient for test stability.

While reducing the timeout in tests is good for performance, 10ms might be too aggressive and could lead to flaky tests if the copy operation or state updates take longer.

✅ Verification successful

Based on the search results, I can now provide a definitive response since we found the relevant test file that uses this timeout value.

10ms timeout for copy confirmation is sufficient based on test implementation

The copyConfirmationTimeout is only used in tests/integration/components/copyable-anything-test.ts to verify the "COPIED!" text state changes. The test waits for half the timeout to check if "COPIED!" is shown, then waits for the full timeout to verify it reverts to "COPY". Since this is just for UI state changes and doesn't involve actual file operations or network calls, 10ms is adequate for this specific use case.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if there are any timing-related test failures or flaky tests

# Test: Search for timing-related test failures in git history
rg -i "timeout|flaky|timing|race condition" -g "tests/**/*.{js,ts}" -A 5

Length of output: 7714

app/components/course-page/repository-dropdown.ts (3)

3-3: LGTM! Clean import syntax.

The simplified service import is more concise and follows modern Ember conventions.


23-23: LGTM! Clean component rename.

The component rename and corresponding Glint registry update are well-aligned. The new name RepositoryDropdown is more concise while maintaining clarity about its purpose.

Also applies to: 100-100


Line range hint 1-102: Verify complete migration to CopyableAnything.

The clipboard-related code has been removed from this component, but let's verify that the corresponding template properly implements the CopyableAnything component for the repository URL copying functionality.

✅ Verification successful

Migration to CopyableAnything is properly implemented

The template correctly implements the CopyableAnything component for handling repository URL copying functionality. The implementation:

  • Uses CopyableAnything component with proper binding to @activeRepository.cloneUrl
  • Includes appropriate disabled state handling
  • Shows copy feedback with animation
  • Has removed all direct clipboard API usage

The migration is complete and correctly implemented.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the template implements CopyableAnything and old clipboard code is removed

# Test 1: Check if the template uses CopyableAnything
echo "Checking for CopyableAnything usage in template..."
rg -l "CopyableAnything.*cloneUrl" "app/components/course-page/repository-dropdown.hbs"

# Test 2: Verify no direct clipboard API usage remains
echo "Checking for any remaining direct clipboard API usage..."
rg -l "navigator\.clipboard|writeText" "app/components/course-page/repository-dropdown.hbs"

Length of output: 413


Script:

#!/bin/bash
# Let's first check if the template file exists and see its content
echo "Checking if template file exists..."
fd "repository-dropdown.hbs" app/components/course-page/

echo -e "\nChecking template content..."
cat app/components/course-page/repository-dropdown.hbs

# Let's also check for any clipboard-related components or helpers in the template
echo -e "\nChecking for any clipboard-related patterns..."
rg -p "copy|clipboard|CopyableAnything" app/components/course-page/repository-dropdown.hbs

Length of output: 7557

tests/integration/components/copyable-anything-test.ts (2)

1-10: LGTM: Proper test setup with all necessary imports.

The test file is properly configured with all required testing utilities and follows Ember testing best practices.


1-79: Verify test coverage for all clipboard interaction scenarios.

While the test suite covers the core functionality well, consider adding these test cases based on the PR objectives:

  1. Test error handling when clipboard API is not available
  2. Test concurrent copy operations
  3. Test with various content types (text, rich text, etc.)
app/components/course-admin/submissions-page/submission-details/header-container.hbs (3)

69-74: LGTM! Clean implementation of repository URL copying.

The integration of CopyableAnything with the repository URL copy button is well-implemented, maintaining the existing UX while standardizing clipboard interactions.


105-110: LGTM! Consistent implementation of commit SHA copying.

The implementation maintains consistency with other copy buttons and preserves test attributes.


120-133: 🛠️ Refactor suggestion

Consider propagating the disabled state to CopyableAnything.

While the button is correctly disabled when the tree SHA is "Unknown", the CopyableAnything component might still attempt to copy when clicked. Consider passing the disabled state to CopyableAnything to prevent copying attempts when the value is unavailable.

Let's verify if CopyableAnything supports the disabled state:

Consider updating the implementation to:

-            <CopyableAnything @value={{@submission.treeSha}} as |performCopyTask hasRecentlyCopied|>
+            <CopyableAnything 
+              @value={{@submission.treeSha}} 
+              @isDisabled={{eq this.shortSubmissionTreeSha "Unknown"}}
+              as |performCopyTask hasRecentlyCopied|>
✅ Verification successful

Based on the search results, I can now generate the final response since I found evidence that the CopyableAnything component does support the isDisabled property and properly handles it:

  1. The interface in app/components/copyable-anything.ts shows it accepts an isDisabled prop:
export interface CopyableAnythingSignature {
  Args: {
    value?: string | null;
    isDisabled?: boolean;
    onCopied?: OnCopiedCallback;
  };
}
  1. The component implementation shows it respects the disabled state in its copy method:
async copy(): Promise<void> {
  if (this.args.isDisabled) {
    return;
  }
  // ...
}
  1. The test file tests/integration/components/copyable-anything-test.ts confirms this behavior:
test('it does nothing when `@isDisabled` is true', async function (assert) {
  // ...
  assert.ok(callback.notCalled, 'callback was not called when @isDisabled is true');
  assert.strictEqual(await navigator.clipboard.readText(), '', 'clipboard is not written when @isDisabled is true');
});

The review comment is correct. The disabled state should be propagated to CopyableAnything.

When the tree SHA is "Unknown", the button is correctly disabled but the CopyableAnything wrapper can still attempt to copy. The suggested fix to pass the disabled state to CopyableAnything is the right approach to prevent any copy attempts when the value is unavailable.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if CopyableAnything component supports @isDisabled property
ast-grep --pattern 'interface CopyableAnythingArgs {
  $$$
  isDisabled?: boolean;
  $$$
}'

Length of output: 93


Script:

#!/bin/bash
# Let's check the implementation of CopyableAnything component
fd "copyable-anything" --type f

# Then examine its contents
rg -l "CopyableAnything" --type ts --type js --type hbs

# Look for any disabled state handling in the component
rg "isDisabled|disabled" -A 3 -B 3 --type ts --type js --type hbs

Length of output: 105458

app/models/repository.ts (1)

147-149: Verify the usage of isNewAsBoolean in UI components.

The implementation looks good. Let's verify its integration with CopyableAnything.

✅ Verification successful

Let me verify the implementation of CopyableAnything to ensure it's being used correctly.


Let me check the component implementation to understand how isDisabled is handled.


Integration of isNewAsBoolean with CopyableAnything is correctly implemented

The implementation is properly integrated:

  • CopyableAnything component correctly handles the isDisabled prop in its implementation
  • The repository dropdown uses isNewAsBoolean to disable the clone URL copying functionality when a new repository is being created
  • The disabled state is consistently reflected in both the functionality and UI styling
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that isNewAsBoolean is used correctly with CopyableAnything

# Test 1: Find all usages of isNewAsBoolean
echo "Finding all usages of isNewAsBoolean..."
rg "isNewAsBoolean"

# Test 2: Specifically look for usage with CopyableAnything's isDisabled property
echo "Checking usage with CopyableAnything..."
rg -A 3 "CopyableAnything" | rg "isDisabled.*isNewAsBoolean"

Length of output: 669


Script:

#!/bin/bash
# Check the implementation of CopyableAnything component
ast-grep --pattern 'class CopyableAnything {
  $$$
}'

# Find the template for CopyableAnything
fd -e hbs -e handlebars "copyable-anything"

# If found, show its contents
fd -e hbs -e handlebars "copyable-anything" -x cat {}

Length of output: 254


Script:

#!/bin/bash
# Check the component implementation
fd -e ts -e js "copyable-anything" -x cat {}

# Also check the repository-dropdown template where it's being used
cat app/components/course-page/repository-dropdown.hbs

Length of output: 12263


interface Signature {
Element: HTMLDivElement;

Args: {
backgroundColor?: 'gray' | 'white';
code: string;
onCopyButtonClick?: () => void;
onCopyButtonClick?: () => void | Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Inconsistent callback signatures found across copyable components

The Promise return type is not consistently implemented across copyable components:

  • CopyableCode: onCopyButtonClick?: () => void | Promise<void>
  • CopyableTerminalCommand: onCopyButtonClick?: () => void | Promise<void>
  • CopyableTerminalCommandWithVariants: onCopyButtonClick?: () => void (missing Promise)
  • CopyableAnything: Uses OnCopiedCallback = () => void | Promise<void> type

The callback signature should be unified across all copyable components to maintain consistency, especially since this is part of a refactor to unify clipboard interactions.

🔗 Analysis chain

LGTM: Method signature update allows for async callbacks.

The change to allow Promise<void> return type is a good improvement that supports async operations while maintaining backward compatibility.

Let's verify this change is consistently applied across other copyable components:


The previous ast-grep pattern might have been too specific. Let's try a broader search to:

  1. Find all relevant files first
  2. Look for any copy-related callback signatures to verify consistency

The previous search was too broad. Let's focus specifically on the copyable components to verify the consistency of the callback signatures.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if other copyable components have similar callback signatures
# Expect: Consistent Promise<void> | void signatures across components

ast-grep --pattern 'interface $_ {
  $$$
  onCopyButtonClick?: () => $ret;
  $$$
}'

Length of output: 85


Script:

#!/bin/bash
# Let's try a broader search for any interfaces or types with copy-related callbacks
# First, let's find all TypeScript files that might contain copyable components
fd -e ts -e tsx | xargs rg -l "copy|clipboard" --ignore-case

# Then let's search for method signatures related to copy operations
rg "(?:on[A-Z][a-zA-Z]*Copy|copy.*\?:.*=>.*)" -A 2 -B 2

Length of output: 2108


Script:

#!/bin/bash
# Let's specifically look at the copyable components implementations
rg -A 5 -B 5 "onCopyButtonClick|CopyableAnything|CopyableCode" app/components/copyable-*.ts

# Also check the interfaces/types in these files
ast-grep --pattern 'interface $_ {
  $$$
}'  app/components/copyable-*.ts

Length of output: 8455

Comment on lines +25 to +27
stub(navigator.clipboard, 'readText').callsFake(async function () {
return (await cache[0]?.getType(TEXT_MIME_TYPE))?.text() || '';
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding error handling for invalid clipboard items.

The readText implementation could benefit from explicit error handling for malformed clipboard items.

Consider this implementation:

 stub(navigator.clipboard, 'readText').callsFake(async function () {
-    return (await cache[0]?.getType(TEXT_MIME_TYPE))?.text() || '';
+    try {
+      const item = cache[0];
+      if (!item) return '';
+      const blob = await item.getType(TEXT_MIME_TYPE);
+      if (!blob) return '';
+      return await blob.text();
+    } catch (error) {
+      console.error('Error reading clipboard text:', error);
+      return '';
+    }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stub(navigator.clipboard, 'readText').callsFake(async function () {
return (await cache[0]?.getType(TEXT_MIME_TYPE))?.text() || '';
});
stub(navigator.clipboard, 'readText').callsFake(async function () {
try {
const item = cache[0];
if (!item) return '';
const blob = await item.getType(TEXT_MIME_TYPE);
if (!blob) return '';
return await blob.text();
} catch (error) {
console.error('Error reading clipboard text:', error);
return '';
}
});

Comment on lines +15 to +45
export default function stubLocalStorage(cache: Map<string, string> = new Map<string, string>()): Map<string, string> {
stub(window.localStorage, 'getItem').callsFake(function (key) {
if (cache.has(key)) {
return String(cache.get(key));
} else {
return null;
}
});

stub(window.localStorage, 'setItem').callsFake(function (key, value) {
cache.set(key, value);
});

stub(window.localStorage, 'removeItem').callsFake(function (key) {
cache.delete(key);
});

stub(window.localStorage, 'clear').callsFake(function () {
cache.clear();
});

stub(window.localStorage, 'key').callsFake(function (index) {
return [...cache.keys()][index] || null;
});

stub(window.localStorage['__proto__'], 'length').get(function () {
return cache.size;
});

return cache;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding QuotaExceededError simulation.

The stub could be more realistic by simulating storage quota errors that can occur in real localStorage.

Consider adding a configurable storage limit:

export default function stubLocalStorage(
  cache: Map<string, string> = new Map<string, string>(),
  options: { quotaInBytes?: number } = {}
): Map<string, string> {
  const calculateSize = () => 
    [...cache.entries()].reduce((size, [k, v]) => 
      size + k.length + v.length, 0);

  stub(window.localStorage, 'setItem').callsFake(function (key, value) {
    const newSize = calculateSize() + String(key).length + String(value).length;
    if (options.quotaInBytes && newSize > options.quotaInBytes) {
      throw new DOMException('QuotaExceededError', 'QuotaExceededError');
    }
    cache.set(String(key), String(value));
  });
  // ... rest of the implementation
}

Comment on lines +24 to +26
stub(window.localStorage, 'setItem').callsFake(function (key, value) {
cache.set(key, value);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enforce string conversion in setItem stub.

The real localStorage.setItem automatically converts values to strings, but this stub doesn't replicate that behavior.

Apply this fix:

 stub(window.localStorage, 'setItem').callsFake(function (key, value) {
-  cache.set(key, value);
+  cache.set(String(key), String(value));
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stub(window.localStorage, 'setItem').callsFake(function (key, value) {
cache.set(key, value);
});
stub(window.localStorage, 'setItem').callsFake(function (key, value) {
cache.set(String(key), String(value));
});

Comment on lines +36 to +38
stub(window.localStorage, 'key').callsFake(function (index) {
return [...cache.keys()][index] || null;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize key method implementation for large datasets.

Using spread operator with cache.keys() creates a temporary array of all keys, which could be inefficient for large datasets.

Consider this more efficient implementation:

 stub(window.localStorage, 'key').callsFake(function (index) {
-  return [...cache.keys()][index] || null;
+  if (index < 0 || index >= cache.size) return null;
+  let i = 0;
+  for (const key of cache.keys()) {
+    if (i === index) return key;
+    i++;
+  }
+  return null;
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stub(window.localStorage, 'key').callsFake(function (index) {
return [...cache.keys()][index] || null;
});
stub(window.localStorage, 'key').callsFake(function (index) {
if (index < 0 || index >= cache.size) return null;
let i = 0;
for (const key of cache.keys()) {
if (i === index) return key;
i++;
}
return null;
});

Comment on lines +32 to +33
await navigator.clipboard.writeText(String(this.args.value));
this.hasRecentlyCopied = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for clipboard API.

The clipboard API can fail in various scenarios (permissions, secure context). Consider adding try-catch and user feedback.

-    await navigator.clipboard.writeText(String(this.args.value));
-    this.hasRecentlyCopied = true;
+    try {
+      await navigator.clipboard.writeText(String(this.args.value));
+      this.hasRecentlyCopied = true;
+    } catch (error) {
+      console.error('Failed to copy to clipboard:', error);
+      // Optionally trigger error feedback
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await navigator.clipboard.writeText(String(this.args.value));
this.hasRecentlyCopied = true;
try {
await navigator.clipboard.writeText(String(this.args.value));
this.hasRecentlyCopied = true;
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// Optionally trigger error feedback
return;
}

Comment on lines +23 to +34
test('it calls an optional `@onCopied` callback after copying', async function (assert) {
const callback = spy();
this.set('onCopiedCallback', callback);
await render(hbs`
{{! @glint-expect-error it thinks onCopiedCallback is not defined}}
<CopyableAnything @onCopied={{this.onCopiedCallback}} as |performCopyTask|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
</CopyableAnything>
`);
await click('[data-test-copy-button]');
assert.ok(callback.calledOnce, 'callback was called once');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the Glint type error instead of suppressing it.

The @glint-expect-error comment suggests a typing issue. Instead of suppressing it, consider fixing the root cause:

  1. Define proper types for the component's arguments
  2. Ensure the test context type includes the callback property

Add proper type definitions:

interface TestContext {
  onCopiedCallback: () => void;
}

test('it calls an optional `@onCopied` callback after copying', async function (this: TestContext, assert) {
  const callback = spy();
  this.onCopiedCallback = callback;
  // ... rest of the test
});

Comment on lines +54 to +78
/**
* Requires browser to be in focus
*/
test('it does nothing when `@isDisabled` is true', async function (assert) {
const callback = spy();
this.set('onCopiedCallback', callback);
await render(hbs`
<CopyableAnything
@value="Test value for copying"
@isDisabled={{true}}
{{! @glint-expect-error it thinks onCopiedCallback is not defined}}
@onCopied={{this.onCopiedCallback}}
as |performCopyTask hasRecentlyCopied|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
<span data-test-copy-button-text>{{if hasRecentlyCopied "COPIED!" "COPY"}}</span>
</CopyableAnything>
`);
click('[data-test-copy-button]'); // Do not await
await timeout(config.x.copyConfirmationTimeout / 2); // Await for hasRecentlyCopied to propagate
assert.dom('[data-test-copy-button-text]').hasText('COPY');
await timeout(config.x.copyConfirmationTimeout); // Await for hasRecentlyCopied to revert
assert.dom('[data-test-copy-button-text]').hasText('COPY');
assert.ok(callback.notCalled, 'callback was not called when @isDisabled is true');
assert.strictEqual(await navigator.clipboard.readText(), '', 'clipboard is not written when @isDisabled is true');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance disabled state test and maintain consistency.

The test is comprehensive but could be improved:

  1. Add assertion for button's disabled state
  2. Fix the Glint type error (same as previous test)
  3. Add aria-disabled attribute check
   test('it does nothing when `@isDisabled` is true', async function (assert) {
     const callback = spy();
     this.set('onCopiedCallback', callback);
     await render(hbs`
       <CopyableAnything
         @value="Test value for copying"
         @isDisabled={{true}}
-        {{! @glint-expect-error it thinks onCopiedCallback is not defined}}
         @onCopied={{this.onCopiedCallback}}
       as |performCopyTask hasRecentlyCopied|>
-        <button data-test-copy-button type="button" {{on "click" performCopyTask}} />
+        <button 
+          data-test-copy-button 
+          type="button"
+          aria-disabled="true"
+          {{on "click" performCopyTask}} 
+        />
         <span data-test-copy-button-text>{{if hasRecentlyCopied "COPIED!" "COPY"}}</span>
       </CopyableAnything>
     `);
+    assert.dom('[data-test-copy-button]').hasAttribute('aria-disabled', 'true', 'Button should have aria-disabled attribute');
     click('[data-test-copy-button]'); // Do not await
     await timeout(config.x.copyConfirmationTimeout / 2); // Await for hasRecentlyCopied to propagate
     assert.dom('[data-test-copy-button-text]').hasText('COPY');

Committable suggestion was skipped due to low confidence.

Comment on lines +12 to +21
test('it copies passed `@value` to clipboard when yielded `performCopyTask` is called', async function (assert) {
await render(hbs`
<CopyableAnything @value="Test value for copying" as |performCopyTask|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
</CopyableAnything>
`);
assert.strictEqual(await navigator.clipboard.readText(), '', 'clipboard is initially empty');
await click('[data-test-copy-button]');
assert.strictEqual(await navigator.clipboard.readText(), 'Test value for copying', 'clipboard contains expected text');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider enhancing the basic copy test with additional checks.

While the test covers the basic functionality well, consider these improvements:

  1. Add assertion to verify button is enabled before clicking
  2. Add accessibility attributes (aria-label) to the button
  3. Add error handling for clipboard permission denial
 test('it copies passed `@value` to clipboard when yielded `performCopyTask` is called', async function (assert) {
   await render(hbs`
     <CopyableAnything @value="Test value for copying" as |performCopyTask|>
-      <button data-test-copy-button type="button" {{on "click" performCopyTask}} />
+      <button 
+        data-test-copy-button 
+        type="button" 
+        aria-label="Copy to clipboard"
+        {{on "click" performCopyTask}} 
+      />
     </CopyableAnything>
   `);
+  assert.dom('[data-test-copy-button]').isEnabled('Copy button should be enabled');
   assert.strictEqual(await navigator.clipboard.readText(), '', 'clipboard is initially empty');
-  await click('[data-test-copy-button]');
+  try {
+    await click('[data-test-copy-button]');
+  } catch (error) {
+    assert.notOk(true, 'Failed to copy: Clipboard permission denied');
+  }
   assert.strictEqual(await navigator.clipboard.readText(), 'Test value for copying', 'clipboard contains expected text');
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('it copies passed `@value` to clipboard when yielded `performCopyTask` is called', async function (assert) {
await render(hbs`
<CopyableAnything @value="Test value for copying" as |performCopyTask|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
</CopyableAnything>
`);
assert.strictEqual(await navigator.clipboard.readText(), '', 'clipboard is initially empty');
await click('[data-test-copy-button]');
assert.strictEqual(await navigator.clipboard.readText(), 'Test value for copying', 'clipboard contains expected text');
});
test('it copies passed `@value` to clipboard when yielded `performCopyTask` is called', async function (assert) {
await render(hbs`
<CopyableAnything @value="Test value for copying" as |performCopyTask|>
<button
data-test-copy-button
type="button"
aria-label="Copy to clipboard"
{{on "click" performCopyTask}}
/>
</CopyableAnything>
`);
assert.dom('[data-test-copy-button]').isEnabled('Copy button should be enabled');
assert.strictEqual(await navigator.clipboard.readText(), '', 'clipboard is initially empty');
try {
await click('[data-test-copy-button]');
} catch (error) {
assert.notOk(true, 'Failed to copy: Clipboard permission denied');
}
assert.strictEqual(await navigator.clipboard.readText(), 'Test value for copying', 'clipboard contains expected text');
});

Comment on lines +36 to +52
/**
* Requires browser to be in focus
*/
test('it yields a `hasRecentlyCopied` boolean, true for a configured interval after copying', async function (assert) {
await render(hbs`
<CopyableAnything as |performCopyTask hasRecentlyCopied|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
<span data-test-copy-button-text>{{if hasRecentlyCopied "COPIED!" "COPY"}}</span>
</CopyableAnything>
`);
assert.dom('[data-test-copy-button-text]').hasText('COPY');
click('[data-test-copy-button]'); // Do not await
await timeout(config.x.copyConfirmationTimeout / 2); // Await for hasRecentlyCopied to propagate
assert.dom('[data-test-copy-button-text]').hasText('COPIED!');
await timeout(config.x.copyConfirmationTimeout); // Await for hasRecentlyCopied to revert
assert.dom('[data-test-copy-button-text]').hasText('COPY');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve test reliability for copy indicator state changes.

The test could be flaky due to browser focus requirement and timing dependencies. Consider these improvements:

  1. Add retry logic for focus-dependent operations
  2. Add assertions to verify intermediate states
  3. Document why browser focus is required
-  /**
-   * Requires browser to be in focus
-   */
+  /**
+   * This test requires browser focus because clipboard operations are only
+   * allowed when the document has focus for security reasons.
+   * @see https://w3c.github.io/clipboard-apis/#privacy-considerations
+   */
   test('it yields a `hasRecentlyCopied` boolean, true for a configured interval after copying', async function (assert) {
     await render(hbs`
       <CopyableAnything as |performCopyTask hasRecentlyCopied|>
         <button data-test-copy-button type="button" {{on "click" performCopyTask}} />
         <span data-test-copy-button-text>{{if hasRecentlyCopied "COPIED!" "COPY"}}</span>
       </CopyableAnything>
     `);
     assert.dom('[data-test-copy-button-text]').hasText('COPY');
+    assert.dom('[data-test-copy-button]').isEnabled('Copy button should be enabled');
     click('[data-test-copy-button]'); // Do not await
     await timeout(config.x.copyConfirmationTimeout / 2); // Await for hasRecentlyCopied to propagate
     assert.dom('[data-test-copy-button-text]').hasText('COPIED!');
+    assert.dom('[data-test-copy-button]').isEnabled('Button should remain enabled after copy');
     await timeout(config.x.copyConfirmationTimeout); // Await for hasRecentlyCopied to revert
     assert.dom('[data-test-copy-button-text]').hasText('COPY');
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Requires browser to be in focus
*/
test('it yields a `hasRecentlyCopied` boolean, true for a configured interval after copying', async function (assert) {
await render(hbs`
<CopyableAnything as |performCopyTask hasRecentlyCopied|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
<span data-test-copy-button-text>{{if hasRecentlyCopied "COPIED!" "COPY"}}</span>
</CopyableAnything>
`);
assert.dom('[data-test-copy-button-text]').hasText('COPY');
click('[data-test-copy-button]'); // Do not await
await timeout(config.x.copyConfirmationTimeout / 2); // Await for hasRecentlyCopied to propagate
assert.dom('[data-test-copy-button-text]').hasText('COPIED!');
await timeout(config.x.copyConfirmationTimeout); // Await for hasRecentlyCopied to revert
assert.dom('[data-test-copy-button-text]').hasText('COPY');
});
/**
* This test requires browser focus because clipboard operations are only
* allowed when the document has focus for security reasons.
* @see https://w3c.github.io/clipboard-apis/#privacy-considerations
*/
test('it yields a `hasRecentlyCopied` boolean, true for a configured interval after copying', async function (assert) {
await render(hbs`
<CopyableAnything as |performCopyTask hasRecentlyCopied|>
<button data-test-copy-button type="button" {{on "click" performCopyTask}} />
<span data-test-copy-button-text>{{if hasRecentlyCopied "COPIED!" "COPY"}}</span>
</CopyableAnything>
`);
assert.dom('[data-test-copy-button-text]').hasText('COPY');
assert.dom('[data-test-copy-button]').isEnabled('Copy button should be enabled');
click('[data-test-copy-button]'); // Do not await
await timeout(config.x.copyConfirmationTimeout / 2); // Await for hasRecentlyCopied to propagate
assert.dom('[data-test-copy-button-text]').hasText('COPIED!');
assert.dom('[data-test-copy-button]').isEnabled('Button should remain enabled after copy');
await timeout(config.x.copyConfirmationTimeout); // Await for hasRecentlyCopied to revert
assert.dom('[data-test-copy-button-text]').hasText('COPY');
});

@rohitpaulk rohitpaulk removed their request for review November 6, 2024 10:59
@rohitpaulk
Copy link
Member

Dismissing review, since our plan was to make a more minimal changeset

@VasylMarchuk VasylMarchuk deleted the stub-clipboard-during-tests branch November 8, 2024 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants