From 1a76b0a4e921708fc38882158458b1663687a0b6 Mon Sep 17 00:00:00 2001 From: hcphat Date: Thu, 21 May 2026 10:42:56 +0700 Subject: [PATCH 1/2] =?UTF-8?q?2.2.=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E5=90=8D=E3=81=AB=E6=97=A5=E6=9C=AC=E8=AA=9E?= =?UTF-8?q?=E3=82=92=E5=85=A5=E5=8A=9B=E3=81=99=E3=82=8B=E9=9A=9B=E3=81=AE?= =?UTF-8?q?=E6=94=B9=E5=96=84:=20Commit=20code=20improve=20project=20name?= =?UTF-8?q?=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/new-project-modal/component.ts | 14 +++ .../components/new-project-modal/template.hbs | 2 +- .../new-project-modal/component-test.ts | 96 ++++++++++++++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/lib/osf-components/addon/components/new-project-modal/component.ts b/lib/osf-components/addon/components/new-project-modal/component.ts index 07a840328..b53cefc47 100644 --- a/lib/osf-components/addon/components/new-project-modal/component.ts +++ b/lib/osf-components/addon/components/new-project-modal/component.ts @@ -215,4 +215,18 @@ export default class NewProjectModal extends Component { searchNodes(this: NewProjectModal, searchTerm: string) { return this.get('searchUserNodesTask').perform(searchTerm); } + + @action + handleKeydown(event: KeyboardEvent & { + isComposing?: boolean; + keyCode?: number; }) { + const isIME = event.isComposing || event.keyCode === 229; + if (isIME) { + return; + } + if (event.key === 'Enter') { + event.preventDefault(); + this.create(); + } + } } diff --git a/lib/osf-components/addon/components/new-project-modal/template.hbs b/lib/osf-components/addon/components/new-project-modal/template.hbs index ccf72b616..06a0d06ab 100644 --- a/lib/osf-components/addon/components/new-project-modal/template.hbs +++ b/lib/osf-components/addon/components/new-project-modal/template.hbs @@ -41,7 +41,7 @@ @required={{true}} @value={{this.nodeTitle}} @type='text' - @enter={{action this.create}} + @keyDown={{action this.handleKeydown}} /> diff --git a/tests/integration/components/new-project-modal/component-test.ts b/tests/integration/components/new-project-modal/component-test.ts index 6db8a2e06..e3872af65 100644 --- a/tests/integration/components/new-project-modal/component-test.ts +++ b/tests/integration/components/new-project-modal/component-test.ts @@ -1,10 +1,15 @@ import { A } from '@ember/array'; import Service from '@ember/service'; -import { render } from '@ember/test-helpers'; +import { + fillIn, + render, + triggerEvent, +} from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import { TestContext } from 'ember-test-helpers'; import { module, test } from 'qunit'; +import sinon from 'sinon'; const currentUserStub = Service.extend({ user: Object.freeze({ @@ -16,12 +21,95 @@ module('Integration | Component | new-project-modal', hooks => { setupRenderingTest(hooks); hooks.beforeEach(function(this: TestContext) { - this.owner.register('service:current-user', currentUserStub); + this.owner.register( + 'service:current-user', + currentUserStub, + ); }); test('it renders', async assert => { - await render(hbs``); + await render(hbs` + + `); + assert.dom('.modal').exists(); - assert.dom('.modal-title').hasText('Create new project'); + assert.dom('.modal-title') + .hasText('Create new project'); + }); + + test('create button is disabled initially', async assert => { + await render(hbs` + + `); + + assert.dom( + '[data-test-create-project-submit]', + ).isDisabled(); + }); + + test('create button is enabled after input', async assert => { + await render(hbs` + + `); + + await fillIn( + '[data-test-new-project-title]', + 'Hello World', + ); + + assert.dom( + '[data-test-create-project-submit]', + ).isEnabled(); + }); + + test('Enter triggers create when not composing', function(this: TestContext, assert) { + const component = this.owner.factoryFor( + 'component:new-project-modal', + )!.create() as any; + + let prevented = false; + + component.handleKeydown({ + key: 'Enter', + isComposing: false, + keyCode: 13, + preventDefault() { + prevented = true; + }, + }); + + assert.ok(prevented); + }); + + test('IME Enter does not trigger create', async function(this: TestContext, assert) { + await render(hbs` + + `); + + const component = this.owner.lookup( + 'component:new-project-modal', + ); + + const createStub = sinon.stub( + component, + 'create', + ); + + const input = document.querySelector( + '[data-test-new-project-title]', + ) as HTMLInputElement; + + await fillIn( + '[data-test-new-project-title]', + 'プロジェクト', + ); + + await triggerEvent(input, 'keydown', { + key: 'Enter', + isComposing: true, + keyCode: 229, + }); + + assert.ok(createStub.notCalled); }); }); From fd7edfe07260ada89176ad525bd1130dd6bdd1b0 Mon Sep 17 00:00:00 2001 From: hcphat Date: Thu, 28 May 2026 09:55:30 +0700 Subject: [PATCH 2/2] =?UTF-8?q?Ref:=202.2.=E3=83=97=E3=83=AD=E3=82=B8?= =?UTF-8?q?=E3=82=A7=E3=82=AF=E3=83=88=E5=90=8D=E3=81=AB=E6=97=A5=E6=9C=AC?= =?UTF-8?q?=E8=AA=9E=E3=82=92=E5=85=A5=E5=8A=9B=E3=81=99=E3=82=8B=E9=9A=9B?= =?UTF-8?q?=E3=81=AE=E6=94=B9=E5=96=84:=20Commit=20code=20rework=20comment?= =?UTF-8?q?=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/new-project-modal/component.ts | 10 +- .../components/new-project-modal/template.hbs | 2 +- .../new-project-modal/component-test.ts | 182 ++++++++++-------- 3 files changed, 105 insertions(+), 89 deletions(-) diff --git a/lib/osf-components/addon/components/new-project-modal/component.ts b/lib/osf-components/addon/components/new-project-modal/component.ts index b53cefc47..57ac64514 100644 --- a/lib/osf-components/addon/components/new-project-modal/component.ts +++ b/lib/osf-components/addon/components/new-project-modal/component.ts @@ -217,14 +217,16 @@ export default class NewProjectModal extends Component { } @action - handleKeydown(event: KeyboardEvent & { - isComposing?: boolean; - keyCode?: number; }) { + handleKeydown(_: string, event: KeyboardEvent & { isComposing?: boolean }) { + if (!event) { + return; + } + const isIME = event.isComposing || event.keyCode === 229; if (isIME) { return; } - if (event.key === 'Enter') { + if (event.key === 'Enter' && this.nodeTitle && this.nodeTitle.trim()) { event.preventDefault(); this.create(); } diff --git a/lib/osf-components/addon/components/new-project-modal/template.hbs b/lib/osf-components/addon/components/new-project-modal/template.hbs index 06a0d06ab..aacb0bbae 100644 --- a/lib/osf-components/addon/components/new-project-modal/template.hbs +++ b/lib/osf-components/addon/components/new-project-modal/template.hbs @@ -41,7 +41,7 @@ @required={{true}} @value={{this.nodeTitle}} @type='text' - @keyDown={{action this.handleKeydown}} + @key-down={{action this.handleKeydown}} /> diff --git a/tests/integration/components/new-project-modal/component-test.ts b/tests/integration/components/new-project-modal/component-test.ts index e3872af65..90c60498a 100644 --- a/tests/integration/components/new-project-modal/component-test.ts +++ b/tests/integration/components/new-project-modal/component-test.ts @@ -1,115 +1,129 @@ import { A } from '@ember/array'; +import EmberObject, { action } from '@ember/object'; import Service from '@ember/service'; -import { - fillIn, - render, - triggerEvent, -} from '@ember/test-helpers'; +import { fillIn, render, triggerEvent, triggerKeyEvent } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import { TestContext } from 'ember-test-helpers'; import { module, test } from 'qunit'; +import Promise from 'rsvp'; import sinon from 'sinon'; -const currentUserStub = Service.extend({ - user: Object.freeze({ - institutions: A([]), - }), -}); +import NewProjectModal from 'osf-components/components/new-project-modal/component'; + +interface LocalTestContext extends TestContext { + createActionSpy: sinon.SinonSpy; +} + +let createSpyPlaceholder: sinon.SinonSpy | undefined; + +class MockNewProjectModal extends NewProjectModal { + @action + create(this: any, ...args: any[]) { + if (createSpyPlaceholder) { + createSpyPlaceholder(...args); + } + } +} module('Integration | Component | new-project-modal', hooks => { setupRenderingTest(hooks); - hooks.beforeEach(function(this: TestContext) { - this.owner.register( - 'service:current-user', - currentUserStub, - ); + hooks.beforeEach(function(this: LocalTestContext) { + this.createActionSpy = sinon.spy(); + createSpyPlaceholder = this.createActionSpy; + + this.owner.register('component:new-project-modal', MockNewProjectModal); + + const mockUser = EmberObject.create({ + institutions: A([EmberObject.create({ id: 'mock' })]), + defaultRegion: EmberObject.create({ id: 'us' }), + }); + + this.owner.register('service:current-user', Service.extend({ user: mockUser })); + this.owner.register('service:features', Service.extend({ isEnabled: () => false })); + this.owner.register('service:intl', Service.extend({ t: (key: string) => key })); + this.owner.register('service:toast', Service.extend({ error: sinon.spy(), success: sinon.spy() })); + this.owner.register('service:analytics', Service.extend({ click: sinon.spy() })); + this.owner.register('service:store', Service.extend({ + findAll() { return Promise.resolve(A([])); }, + })); + }); + + hooks.afterEach(() => { + createSpyPlaceholder = undefined; }); test('it renders', async assert => { await render(hbs` - + `); - assert.dom('.modal').exists(); - assert.dom('.modal-title') - .hasText('Create new project'); + assert.dom('.modal-title').hasText('Create new project'); }); test('create button is disabled initially', async assert => { await render(hbs` - + `); - - assert.dom( - '[data-test-create-project-submit]', - ).isDisabled(); + assert.dom('[data-test-create-project-submit]').isDisabled(); }); - test('create button is enabled after input', async assert => { + test('it renders and initializes correctly', async assert => { await render(hbs` - + `); - await fillIn( - '[data-test-new-project-title]', - 'Hello World', - ); - - assert.dom( - '[data-test-create-project-submit]', - ).isEnabled(); + assert.dom('[data-test-new-project-title]').exists('Input fields for project title exists'); }); - test('Enter triggers create when not composing', function(this: TestContext, assert) { - const component = this.owner.factoryFor( - 'component:new-project-modal', - )!.create() as any; - - let prevented = false; - - component.handleKeydown({ - key: 'Enter', - isComposing: false, - keyCode: 13, - preventDefault() { - prevented = true; - }, - }); - - assert.ok(prevented); - }); - - test('IME Enter does not trigger create', async function(this: TestContext, assert) { - await render(hbs` - - `); - - const component = this.owner.lookup( - 'component:new-project-modal', - ); - - const createStub = sinon.stub( - component, - 'create', - ); - - const input = document.querySelector( - '[data-test-new-project-title]', - ) as HTMLInputElement; - - await fillIn( - '[data-test-new-project-title]', - 'プロジェクト', - ); - - await triggerEvent(input, 'keydown', { - key: 'Enter', - isComposing: true, - keyCode: 229, - }); - - assert.ok(createStub.notCalled); - }); + test( + 'Enter key triggers create action through DOM binding when not composing', + async function(this: LocalTestContext, assert) { + await render(hbs` + + `); + + const inputSelector = '[data-test-new-project-title]'; + assert.dom(inputSelector).exists('Input field must exist'); + + await fillIn(inputSelector, 'My Awesome New Project'); + + await triggerKeyEvent(inputSelector, 'keydown', 'Enter'); + + assert.ok( + this.createActionSpy.calledOnce, + 'The create() action on the component should be triggered.', + ); + }, + ); + + test( + 'Enter key does NOT trigger create action when Japanese IME is composing', + async function(this: LocalTestContext, assert) { + await render(hbs` + + `); + + const inputSelector = '[data-test-new-project-title]'; + assert.dom(inputSelector).exists(); + + await fillIn(inputSelector, 'プロジェクト'); + + await triggerEvent(inputSelector, 'keydown', { + key: 'Enter', + code: 'Enter', + keyCode: 229, + which: 229, + isComposing: true, + bubbles: true, + cancelable: true, + } as any); + + assert.notOk( + this.createActionSpy.called, + 'The create() action must NOT be triggered during IME composition.', + ); + }, + ); });