Skip to content

Fix PIN input displaying double asterisks for each digit - #114

Merged
tomkp merged 1 commit into
masterfrom
fix/pin-double-asterisks
Dec 31, 2025
Merged

tomkp merged 1 commit into
masterfrom
fix/pin-double-asterisks

Conversation

@tomkp

@tomkp tomkp commented Dec 31, 2025

Copy link
Copy Markdown
Owner

Summary

  • Fixes bug where each PIN digit showed 2 asterisks instead of 1
  • Removed redundant maskedPin text element - TextInput already handles masking via its mask prop
  • Added regression test to prevent this from happening again

Root Cause

The PinScreen component was rendering both:

  1. A manually created maskedPin ('•'.repeat(pin.length))
  2. The TextInput component with mask="•"

Both were displayed simultaneously, causing double masking.

Test Plan

  • Added test that verifies PinScreen doesn't render both maskedPin and TextInput with mask
  • All existing tests pass

Fixes #113

The PinScreen component was rendering both a manually created maskedPin
text element and a TextInput component with mask prop, causing each digit
to display twice as many asterisks.

Removed the redundant maskedPin rendering - TextInput already handles
the masking via its mask prop.

Fixes #113
Copilot AI review requested due to automatic review settings December 31, 2025 11:37
@tomkp
tomkp merged commit 2969308 into master Dec 31, 2025
6 checks passed
@tomkp
tomkp deleted the fix/pin-double-asterisks branch December 31, 2025 11:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug where the PIN input field displayed two asterisks for each digit entered. The root cause was that both a manually rendered maskedPin text element and the TextInput component's built-in mask prop were being displayed simultaneously.

  • Removed the redundant maskedPin variable and its rendering
  • Simplified the JSX structure by directly rendering the TextInput component
  • Added a regression test to detect if this double-masking pattern reappears

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/interactive.tsx Removed redundant maskedPin variable and simplified PinScreen JSX to only use TextInput's built-in mask prop
src/interactive.test.ts Added regression test using static source code analysis to detect the double-masking pattern

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/interactive.test.ts
Comment on lines +10 to +11
// Get the source directory (tests run from dist/, source is in src/)
const srcDir = __dirname.replace(/\/dist$/, '/src');

Copilot AI Dec 31, 2025

Copy link

Choose a reason for hiding this comment

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

The path resolution logic assumes tests run from the dist/ directory, but this may be fragile. The regex replacement /\/dist$/ will fail silently if the test is run from a different location (e.g., during development from the project root). Consider using a more robust approach like resolving relative to the test file location using join(__dirname, '..', '..', 'src') or checking if the path actually exists before attempting to read the file.

Suggested change
// Get the source directory (tests run from dist/, source is in src/)
const srcDir = __dirname.replace(/\/dist$/, '/src');
// Get the source directory: project root is parent of compiled test dir (e.g., dist/), source is in src/
const projectRoot = dirname(__dirname);
const srcDir = join(projectRoot, 'src');

Copilot uses AI. Check for mistakes.
Comment thread src/interactive.test.ts
const source = readFileSync(sourceFile, 'utf-8');

// Find the PinScreen function body
const pinScreenMatch = source.match(/function PinScreen\([^)]*\)[^{]*\{([\s\S]*?)^function /m);

Copilot AI Dec 31, 2025

Copy link

Choose a reason for hiding this comment

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

The regex pattern /function PinScreen\([^)]*\)[^{]*\{([\s\S]*?)^function /m is brittle and may break if the code is refactored (e.g., arrow functions, multiple functions on the same line, or minified code). Additionally, it captures everything until the next function declaration, which could fail if there are nested functions or no subsequent function. Consider using static analysis tools or AST parsing libraries like @babel/parser for more reliable code inspection, or alternatively, test the actual runtime behavior instead of parsing source code.

Copilot uses AI. Check for mistakes.
Comment thread src/interactive.test.ts
Comment on lines +22 to +46
it('should not render both manual maskedPin and TextInput with mask prop (causes double asterisks)', () => {
// Read the source file to check for the bug pattern
const sourceFile = join(srcDir, 'interactive.tsx');
const source = readFileSync(sourceFile, 'utf-8');

// Find the PinScreen function body
const pinScreenMatch = source.match(/function PinScreen\([^)]*\)[^{]*\{([\s\S]*?)^function /m);
assert.ok(pinScreenMatch, 'PinScreen function should exist');

const pinScreenBody = pinScreenMatch[1];

// Check if there's a maskedPin variable being rendered alongside TextInput with mask
const hasMaskedPinRendered = /\{maskedPin\}/.test(pinScreenBody ?? '');
const hasTextInputWithMask = /TextInput[\s\S]*?mask=/.test(pinScreenBody ?? '');

// The bug is when both maskedPin is rendered AND TextInput has mask prop
const hasBug = hasMaskedPinRendered && hasTextInputWithMask;

assert.strictEqual(
hasBug,
false,
'PinScreen should not render both maskedPin text and TextInput with mask prop - this causes double asterisks. ' +
'Either remove the maskedPin rendering or remove the mask prop from TextInput.'
);
});

Copilot AI Dec 31, 2025

Copy link

Choose a reason for hiding this comment

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

The test uses static source code analysis instead of testing actual component behavior. This is an anti-pattern because: (1) it tests implementation details rather than behavior, (2) it's fragile and breaks with refactoring, and (3) it doesn't actually verify that the bug is fixed at runtime. For React components, consider using a proper testing approach such as rendering the component with ink-testing-library or at minimum testing that the exported component exists and has the expected structure. Static source analysis should be a last resort for testing.

Copilot uses AI. Check for mistakes.
tomkp added a commit that referenced this pull request Dec 31, 2025
- Add ink-testing-library for proper component testing
- Replace source-code-parsing test with behavioral tests
- Export PinScreen and PinScreenProps for testing
- Add tests that demonstrate the double-masking bug pattern
- Verify TextInput mask prop produces single mask per digit

Addresses Copilot feedback on PR #114
tomkp added a commit that referenced this pull request Dec 31, 2025
* Improve PinScreen testing with ink-testing-library

- Add ink-testing-library for proper component testing
- Replace source-code-parsing test with behavioral tests
- Export PinScreen and PinScreenProps for testing
- Add tests that demonstrate the double-masking bug pattern
- Verify TextInput mask prop produces single mask per digit

Addresses Copilot feedback on PR #114

* Include tsx test files in ESLint relaxed rules
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PIN input displays double asterisks for each digit

2 participants