Skip to content

Commit

Permalink
fix(detection): Copy text affecting style properties of input/textare…
Browse files Browse the repository at this point in the history
…a elements when creating fake (#886)

Fixes #672
  • Loading branch information
melink14 authored Apr 3, 2022
1 parent 4f6374b commit 23fc0df
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 4 deletions.
23 changes: 19 additions & 4 deletions extension/rikaicontent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1066,11 +1066,26 @@ class RcxContent {
} else {
textValue = real.value;
}
const realStyles = window.getComputedStyle(real, '');
fake.innerText = textValue;
fake.style.cssText = document.defaultView!.getComputedStyle(
real,
''
).cssText;
// Text areas never visible collapse spaces so always set whiteSpace to pre-wrap
fake.style.whiteSpace = 'pre-wrap';
fake.style.font = realStyles.font;
fake.style.height = realRect.height + 'px';
// Without a width, the fake div will expand horizontally
fake.style.width = realRect.width + 'px';
// Padding moves text inwards so needs to be copied
fake.style.padding = realStyles.padding;
// Border also affects total width sometimes
fake.style.border = realStyles.border;
// Effects total width when padding and border are set.
fake.style.boxSizing = realStyles.boxSizing;
// The overflow property can add scrollbars which affects text placement.
fake.style.overflow = realStyles.overflow;
fake.style.letterSpacing = realStyles.letterSpacing;
// Japanese text is often not broken into words but this could be important for
// mixed language text.
fake.style.wordSpacing = realStyles.wordSpacing;
fake.scrollTop = real.scrollTop;
fake.scrollLeft = real.scrollLeft;
fake.style.position = 'absolute';
Expand Down
183 changes: 183 additions & 0 deletions extension/test/rikaicontent_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,179 @@ describe('RcxContent', function () {
text: '生test',
});
});

it('triggers xsearch message when text area has custom font styles', function () {
const clock = sinon.useFakeTimers();
const measuringSpan = insertHtmlIntoDomAndReturnFirstTextNode(
'<span style="font-size:32px">位の日本語訳・</span>'
) as HTMLSpanElement;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
'<input style="font-size:32px" value="位の日本語訳・中国語訳にも" size="34"/>'
) as HTMLInputElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: measuringSpan.getBoundingClientRect().width + 1,
y: input.getBoundingClientRect().height / 2,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

// This value is chosen via experimentation since it's hard to know exactly
// where a character is in an input element.
expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '中国語訳にも',
});
});

it('triggers xsearch message when text area has custom width and scrollbars', function () {
const clock = sinon.useFakeTimers();
const measuringDiv = insertHtmlIntoDomAndReturnFirstTextNode(
'<div style="width:40px;height:auto;overflow-y:scroll">位の</div>'
) as HTMLDivElement;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
'<textarea style="all:initial;width:40px;height:100px">位の日本語訳・中国語訳にも'
) as HTMLTextAreaElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: 0,
y: measuringDiv.getBoundingClientRect().height + 1,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '日本語訳・中国語訳にも',
});
});

it('does not trigger xsearch for text that should be truncated by custom height', function () {
const clock = sinon.useFakeTimers();
const measuringDiv = insertHtmlIntoDomAndReturnFirstTextNode(
'<div style="width:40px;height:auto;overflow-y:scroll">位の日本語訳・中国語訳にも</div>'
) as HTMLDivElement;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
'<textarea style="all:initial;width:40px;height:100px">位の日本語訳・中国語訳にも'
) as HTMLTextAreaElement;

// Trigger at the the bottom of full length text even though the end should
// be truncated if height is honored.
triggerMousemoveAtElementStartWithOffset(input, {
x: 5,
y: measuringDiv.getBoundingClientRect().height - 1,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.not.been.called;
});

it('triggers xsearch message when text area has custom padding', function () {
const clock = sinon.useFakeTimers();
const padding = 20;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
`<textarea style="padding:${padding}px">中国語訳にも`
) as HTMLTextAreaElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: padding + 1,
y: padding + 1,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '中国語訳にも',
});
});

it('triggers xsearch message when text area has custom border', function () {
const clock = sinon.useFakeTimers();
const border = 20;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
`<textarea style="border:${border}px solid">中国語訳にも`
) as HTMLTextAreaElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: border + 1,
y: border + 1,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '中国語訳にも',
});
});

it('triggers xsearch message when above Japanese text preceded by consecutive spaces', function () {
const clock = sinon.useFakeTimers();
const measuringSpan = insertHtmlIntoDomAndReturnFirstTextNode(
'<span style="white-space:pre-wrap">中国 </span>'
) as HTMLDivElement;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
'<textarea style="all:initial;white-space:normal">中国 語訳にも'
) as HTMLTextAreaElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: measuringSpan.getBoundingClientRect().width + 1,
y: measuringSpan.getBoundingClientRect().height / 2,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '語訳にも',
});
});

it('triggers xsearch message when text area has custom letter-spacing', function () {
const clock = sinon.useFakeTimers();
const measuringSpan = insertHtmlIntoDomAndReturnFirstTextNode(
'<span style="letter-spacing:10px">中国</span>'
) as HTMLDivElement;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
'<textarea style="all:initial;letter-spacing:10px">中国語訳にも'
) as HTMLTextAreaElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: measuringSpan.getBoundingClientRect().width + 1,
y: measuringSpan.getBoundingClientRect().height / 2,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '語訳にも',
});
});

it('triggers xsearch message when text area has custom word-spacing', function () {
const clock = sinon.useFakeTimers();
const measuringSpan = insertHtmlIntoDomAndReturnFirstTextNode(
'<span style="word-spacing:10px">a a a 中国</span>'
) as HTMLDivElement;
const input = insertHtmlIntoDomAndReturnFirstTextNode(
'<textarea style="all:initial;word-spacing:10px">a a a 中国語訳にも'
) as HTMLTextAreaElement;

triggerMousemoveAtElementStartWithOffset(input, {
x: measuringSpan.getBoundingClientRect().width + 1,
y: measuringSpan.getBoundingClientRect().height / 2,
});
// Tick the clock forward to account for the popup delay.
clock.tick(1);

expect(chrome.runtime.sendMessage).to.have.been.calledWithMatch({
type: 'xsearch',
text: '語訳にも',
});
});
});

describe('with Google Docs annotated canvas', function () {
Expand Down Expand Up @@ -796,6 +969,16 @@ function triggerMousemoveAtElementCenter(element: Element) {
});
}

function triggerMousemoveAtElementStartWithOffset(
element: Element,
offset: { x: number; y: number }
) {
simulant.fire(element, 'mousemove', {
clientX: Math.ceil(element.getBoundingClientRect().left + offset.x),
clientY: Math.ceil(element.getBoundingClientRect().top + offset.y),
});
}

function insertHtmlIntoDomAndReturnFirstTextNode(htmlString: string): Node {
const template = document.createElement('template');
template.innerHTML = htmlString;
Expand Down

0 comments on commit 23fc0df

Please sign in to comment.