| 1 |
// The content fingerprint is the client half of the same-type misresolve |
| 2 |
// guard (SaveController / WPNavigationController). It carries the visible text |
| 3 |
// of the block the user actually clicked so the server can refuse a save that |
| 4 |
// resolved — by count — to a different block of the same type. normalizeText |
| 5 |
// must mirror BlockFingerprint::normalize on the PHP side. |
| 6 |
|
| 7 |
import { |
| 8 |
normalizedTextEquals, |
| 9 |
normalizeText, |
| 10 |
textFingerprint, |
| 11 |
} from '@quick-edit/lib/fingerprint'; |
| 12 |
|
| 13 |
describe('normalizeText', () => { |
| 14 |
it('collapses whitespace runs to single spaces and trims', () => { |
| 15 |
expect(normalizeText(' hello world \n')).toBe('hello world'); |
| 16 |
}); |
| 17 |
|
| 18 |
it('treats null/undefined as empty', () => { |
| 19 |
expect(normalizeText(null)).toBe(''); |
| 20 |
expect(normalizeText(undefined)).toBe(''); |
| 21 |
}); |
| 22 |
|
| 23 |
// the_content runs wptexturize on the rendered DOM the client reads, so a |
| 24 |
// straight-quote source renders with smart quotes / dashes / ellipsis. Fold |
| 25 |
// them back so the fingerprint matches the raw markup the server parses. |
| 26 |
it('folds smart quotes, dashes, and ellipsis (wptexturize tolerance)', () => { |
| 27 |
expect(normalizeText('Woody’s')).toBe("Woody's"); |
| 28 |
expect(normalizeText('‘a’')).toBe("'a'"); |
| 29 |
expect(normalizeText('“quoted”')).toBe('"quoted"'); |
| 30 |
expect(normalizeText('a—b')).toBe('a-b'); |
| 31 |
expect(normalizeText('a–b')).toBe('a-b'); |
| 32 |
expect(normalizeText('a--b')).toBe('a-b'); |
| 33 |
expect(normalizeText('more…')).toBe('more...'); |
| 34 |
expect(normalizeText('a b')).toBe('a b'); |
| 35 |
}); |
| 36 |
}); |
| 37 |
|
| 38 |
describe('normalizedTextEquals', () => { |
| 39 |
// The reveal poll reads the editable (raw, straight quote) and the live |
| 40 |
// element (rendered, wptexturize'd curly quote); the two must compare equal |
| 41 |
// or a smart-quote block strands the reveal on its frame-cap. |
| 42 |
it('matches a raw block against its wptexturize-rendered twin', () => { |
| 43 |
expect( |
| 44 |
normalizedTextEquals( |
| 45 |
"Authentic falafel celebrating Chicago's vibrant food scene.", |
| 46 |
'Authentic falafel celebrating Chicago’s vibrant food scene.', |
| 47 |
), |
| 48 |
).toBe(true); |
| 49 |
}); |
| 50 |
|
| 51 |
it('still matches across whitespace differences', () => { |
| 52 |
expect(normalizedTextEquals(' Shop Desks ', 'Shop Desks')).toBe(true); |
| 53 |
}); |
| 54 |
|
| 55 |
it('treats two empty/blank texts as equal (an empty block being edited)', () => { |
| 56 |
expect(normalizedTextEquals('', ' ')).toBe(true); |
| 57 |
expect(normalizedTextEquals(null, undefined)).toBe(true); |
| 58 |
}); |
| 59 |
|
| 60 |
it('does not match genuinely different text', () => { |
| 61 |
expect(normalizedTextEquals('Welcome to WordPress', 'Shop Desks')).toBe( |
| 62 |
false, |
| 63 |
); |
| 64 |
}); |
| 65 |
}); |
| 66 |
|
| 67 |
describe('textFingerprint', () => { |
| 68 |
it('returns the normalized textContent of the element', () => { |
| 69 |
const el = document.createElement('p'); |
| 70 |
el.textContent = ' Buy now '; |
| 71 |
expect(textFingerprint(el)).toEqual({ text: 'Buy now' }); |
| 72 |
}); |
| 73 |
|
| 74 |
it('returns null when the element has no visible text (fail open)', () => { |
| 75 |
expect(textFingerprint(document.createElement('figure'))).toBeNull(); |
| 76 |
}); |
| 77 |
|
| 78 |
it('returns null for a missing element', () => { |
| 79 |
expect(textFingerprint(null)).toBeNull(); |
| 80 |
}); |
| 81 |
}); |
| 82 |
|