How to use promptBuildCommand method in stryker-parent

Best JavaScript code snippet using stryker-parent

stryker-initializer.spec.ts

Source:stryker-initializer.spec.ts Github

copy

Full Screen

1import childProcess from 'child_process';2import fs from 'fs';3import { syncBuiltinESMExports } from 'module';4import { testInjector } from '@stryker-mutator/test-helpers';5import { childProcessAsPromised, normalizeWhitespaces } from '@stryker-mutator/util';6import { expect } from 'chai';7import inquirer from 'inquirer';8import sinon from 'sinon';9import typedRestClient, { type RestClient, type IRestResponse } from 'typed-rest-client/RestClient.js';10import { fileUtils } from '../../../src/utils/file-utils.js';11import { initializerTokens } from '../../../src/initializer/index.js';12import { NpmClient } from '../../../src/initializer/npm-client.js';13import { PackageInfo } from '../../../src/initializer/package-info.js';14import { Preset } from '../../../src/initializer/presets/preset.js';15import { PresetConfiguration } from '../../../src/initializer/presets/preset-configuration.js';16import { StrykerConfigWriter } from '../../../src/initializer/stryker-config-writer.js';17import { StrykerInitializer } from '../../../src/initializer/stryker-initializer.js';18import { StrykerInquirer } from '../../../src/initializer/stryker-inquirer.js';19import { Mock } from '../../helpers/producers.js';20import { GitignoreWriter } from '../../../src/initializer/gitignore-writer.js';21import { SUPPORTED_CONFIG_FILE_EXTENSIONS } from '../../../src/config/config-file-formats.js';22describe(StrykerInitializer.name, () => {23 let sut: StrykerInitializer;24 let inquirerPrompt: sinon.SinonStub;25 let childExecSync: sinon.SinonStub;26 let childExec: sinon.SinonStub;27 let fsWriteFile: sinon.SinonStubbedMember<typeof fs.promises.writeFile>;28 let existsStub: sinon.SinonStubbedMember<typeof fileUtils['exists']>;29 let restClientPackage: sinon.SinonStubbedInstance<RestClient>;30 let restClientSearch: sinon.SinonStubbedInstance<RestClient>;31 let gitignoreWriter: sinon.SinonStubbedInstance<GitignoreWriter>;32 let out: sinon.SinonStub;33 let presets: Preset[];34 let presetMock: Mock<Preset>;35 beforeEach(() => {36 out = sinon.stub();37 presets = [];38 presetMock = {39 createConfig: sinon.stub(),40 name: 'awesome-preset',41 };42 childExec = sinon.stub(childProcessAsPromised, 'exec');43 inquirerPrompt = sinon.stub(inquirer, 'prompt');44 childExecSync = sinon.stub(childProcess, 'execSync');45 fsWriteFile = sinon.stub(fs.promises, 'writeFile');46 existsStub = sinon.stub(fileUtils, 'exists');47 restClientSearch = sinon.createStubInstance(typedRestClient.RestClient);48 restClientPackage = sinon.createStubInstance(typedRestClient.RestClient);49 gitignoreWriter = sinon.createStubInstance(GitignoreWriter);50 syncBuiltinESMExports();51 sut = testInjector.injector52 .provideValue(initializerTokens.out, out as unknown as typeof console.log)53 .provideValue(initializerTokens.restClientNpm, restClientPackage as unknown as RestClient)54 .provideValue(initializerTokens.restClientNpmSearch, restClientSearch as unknown as RestClient)55 .provideClass(initializerTokens.inquirer, StrykerInquirer)56 .provideClass(initializerTokens.npmClient, NpmClient)57 .provideValue(initializerTokens.strykerPresets, presets)58 .provideClass(initializerTokens.configWriter, StrykerConfigWriter)59 .provideValue(initializerTokens.gitignoreWriter, gitignoreWriter as unknown as GitignoreWriter)60 .injectClass(StrykerInitializer);61 });62 describe('initialize()', () => {63 beforeEach(() => {64 stubTestRunners('@stryker-mutator/awesome-runner', 'stryker-hyper-runner', 'stryker-ghost-runner', '@stryker-mutator/jest-runner');65 stubMutators('@stryker-mutator/typescript', '@stryker-mutator/javascript-mutator');66 stubReporters('stryker-dimension-reporter', '@stryker-mutator/mars-reporter');67 stubPackageClient({68 '@stryker-mutator/awesome-runner': null,69 '@stryker-mutator/javascript-mutator': null,70 '@stryker-mutator/mars-reporter': null,71 '@stryker-mutator/typescript': null,72 '@stryker-mutator/webpack': null,73 'stryker-dimension-reporter': null,74 'stryker-ghost-runner': null,75 'stryker-hyper-runner': {76 files: [],77 someOtherSetting: 'enabled',78 },79 '@stryker-mutator/jest-runner': null,80 });81 fsWriteFile.resolves();82 presets.push(presetMock);83 });84 it('should prompt for preset, test runner, reporters, package manager and config type', async () => {85 arrangeAnswers({86 packageManager: 'yarn',87 reporters: ['dimension', 'mars'],88 testRunner: 'awesome',89 });90 await sut.initialize();91 expect(inquirerPrompt).callCount(6);92 const [93 promptPreset,94 promptTestRunner,95 promptBuildCommand,96 promptReporters,97 promptPackageManagers,98 promptConfigTypes,99 ]: inquirer.ui.FetchedQuestion[] = [100 inquirerPrompt.getCall(0).args[0],101 inquirerPrompt.getCall(1).args[0],102 inquirerPrompt.getCall(2).args[0],103 inquirerPrompt.getCall(3).args[0],104 inquirerPrompt.getCall(4).args[0],105 inquirerPrompt.getCall(5).args[0],106 ];107 expect(promptPreset.type).to.eq('list');108 expect(promptPreset.name).to.eq('preset');109 expect(promptPreset.choices).to.deep.eq(['awesome-preset', new inquirer.Separator(), 'None/other']);110 expect(promptTestRunner.type).to.eq('list');111 expect(promptTestRunner.name).to.eq('testRunner');112 expect(promptTestRunner.choices).to.deep.eq(['awesome', 'hyper', 'ghost', 'jest', new inquirer.Separator(), 'command']);113 expect(promptBuildCommand.name).to.eq('buildCommand');114 expect(promptReporters.type).to.eq('checkbox');115 expect(promptReporters.choices).to.deep.eq(['dimension', 'mars', 'html', 'clear-text', 'progress', 'dashboard']);116 expect(promptPackageManagers.type).to.eq('list');117 expect(promptPackageManagers.choices).to.deep.eq(['npm', 'yarn']);118 expect(promptConfigTypes.type).to.eq('list');119 expect(promptConfigTypes.choices).to.deep.eq(['JSON', 'JavaScript']);120 });121 it('should immediately complete when a preset and package manager is chosen', async () => {122 inquirerPrompt.resolves({123 packageManager: 'npm',124 preset: 'awesome-preset',125 configType: 'JSON',126 });127 resolvePresetConfig();128 await sut.initialize();129 expect(inquirerPrompt).callCount(3);130 expect(out).calledWith('Done configuring stryker. Please review "stryker.conf.json", you might need to configure your test runner correctly.');131 expect(out).calledWith("Let's kill some mutants with this command: `stryker run`");132 });133 it('should correctly write and format the stryker js configuration file', async () => {134 const guideUrl = 'https://awesome-preset.org';135 const config = { awesomeConf: 'awesome' };136 childExec.resolves();137 resolvePresetConfig({138 config,139 guideUrl,140 });141 const expectedOutput = `// @ts-check142 /** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ 143 const config = {144 "_comment": "This config was generated using 'stryker init'. Please see the guide for more information: https://awesome-preset.org",145 "awesomeConf": "${config.awesomeConf}"146 };147 export default config;`;148 inquirerPrompt.resolves({149 packageManager: 'npm',150 preset: 'awesome-preset',151 configType: 'JavaScript',152 });153 await sut.initialize();154 expectStrykerConfWritten(expectedOutput);155 expect(childExec).calledWith('npx prettier --write stryker.conf.mjs');156 });157 it('should handle errors when formatting fails', async () => {158 // Arrange159 const expectedError = new Error('Formatting fails');160 childExec.rejects(expectedError);161 inquirerPrompt.resolves({162 packageManager: 'npm',163 preset: 'awesome-preset',164 configType: 'JavaScript',165 });166 resolvePresetConfig();167 // Act168 await sut.initialize();169 // Assert170 expect(out).calledWith('Unable to format stryker.conf.js file for you. This is not a big problem, but it might look a bit messy 🙈.');171 expect(testInjector.logger.debug).calledWith('Prettier exited with error', expectedError);172 });173 it('should correctly load dependencies from the preset', async () => {174 resolvePresetConfig({ dependencies: ['my-awesome-dependency', 'another-awesome-dependency'] });175 inquirerPrompt.resolves({176 packageManager: 'npm',177 preset: 'awesome-preset',178 configType: 'JSON',179 });180 await sut.initialize();181 expect(fsWriteFile).calledOnce;182 expect(childExecSync).calledWith('npm i --save-dev my-awesome-dependency another-awesome-dependency', { stdio: [0, 1, 2] });183 });184 it('should correctly load configuration from a preset', async () => {185 resolvePresetConfig();186 inquirerPrompt.resolves({187 packageManager: 'npm',188 preset: 'awesome-preset',189 configType: 'JSON',190 });191 await sut.initialize();192 expect(inquirerPrompt).callCount(3);193 const [promptPreset, promptConfigType, promptPackageManager]: inquirer.ui.FetchedQuestion[] = [194 inquirerPrompt.getCall(0).args[0],195 inquirerPrompt.getCall(1).args[0],196 inquirerPrompt.getCall(2).args[0],197 ];198 expect(promptPreset.type).to.eq('list');199 expect(promptPreset.name).to.eq('preset');200 expect(promptPreset.choices).to.deep.eq(['awesome-preset', new inquirer.Separator(), 'None/other']);201 expect(promptConfigType.type).to.eq('list');202 expect(promptConfigType.choices).to.deep.eq(['JSON', 'JavaScript']);203 expect(promptPackageManager.type).to.eq('list');204 expect(promptPackageManager.choices).to.deep.eq(['npm', 'yarn']);205 });206 it('should install any additional dependencies', async () => {207 inquirerPrompt.resolves({208 packageManager: 'npm',209 reporters: ['dimension', 'mars'],210 testRunner: 'awesome',211 configType: 'JSON',212 });213 await sut.initialize();214 expect(out).calledWith('Installing NPM dependencies...');215 expect(childExecSync).calledWith('npm i --save-dev @stryker-mutator/awesome-runner stryker-dimension-reporter @stryker-mutator/mars-reporter', {216 stdio: [0, 1, 2],217 });218 });219 it('should configure testRunner, reporters, and packageManager', async () => {220 inquirerPrompt.resolves({221 packageManager: 'npm',222 reporters: ['dimension', 'mars', 'progress'],223 testRunner: 'awesome',224 configType: 'JSON',225 });226 await sut.initialize();227 expect(fsWriteFile).calledOnce;228 const [fileName, content] = fsWriteFile.getCall(0).args;229 expect(fileName).eq('stryker.conf.json');230 const normalizedContent = normalizeWhitespaces(content as string);231 expect(normalizedContent).contains('"testRunner": "awesome"');232 expect(normalizedContent).contains('"packageManager": "npm"');233 expect(normalizedContent).contains('"coverageAnalysis": "perTest"');234 expect(normalizedContent).contains('"dimension", "mars", "progress"');235 });236 it('should configure the additional settings from the plugins', async () => {237 inquirerPrompt.resolves({238 packageManager: 'npm',239 reporters: [],240 testRunner: 'hyper',241 configType: 'JSON',242 });243 await sut.initialize();244 expect(fs.promises.writeFile).calledWith('stryker.conf.json', sinon.match('"someOtherSetting": "enabled"'));245 expect(fs.promises.writeFile).calledWith('stryker.conf.json', sinon.match('"files": []'));246 });247 it('should annotate the config file with the docs url', async () => {248 inquirerPrompt.resolves({249 packageManager: 'npm',250 reporters: [],251 testRunner: 'hyper',252 configType: 'JSON',253 });254 await sut.initialize();255 expect(fs.promises.writeFile).calledWith(256 'stryker.conf.json',257 sinon.match(258 '"_comment": "This config was generated using \'stryker init\'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information"'259 )260 );261 });262 it('should not prompt for buildCommand if test runner is jest', async () => {263 inquirerPrompt.resolves({264 packageManager: 'npm',265 reporters: ['dimension', 'mars', 'progress'],266 testRunner: 'jest',267 configType: 'JSON',268 buildCommand: 'none',269 });270 await sut.initialize();271 const promptBuildCommand = inquirerPrompt.getCalls().filter((call) => call.args[0].name === 'buildCommand');272 expect(promptBuildCommand.length === 1);273 expect(promptBuildCommand[0].args[0].when).to.be.false;274 expect(fs.promises.writeFile).calledWith(275 'stryker.conf.json',276 sinon.match((val) => !val.includes('"buildCommand": '))277 );278 });279 it('should not write "buildCommand" config option if empty buildCommand entered', async () => {280 inquirerPrompt.resolves({281 packageManager: 'npm',282 reporters: [],283 testRunner: 'hyper',284 configType: 'JSON',285 buildCommand: 'none',286 });287 await sut.initialize();288 expect(fs.promises.writeFile).calledWith(289 'stryker.conf.json',290 sinon.match((val) => !val.includes('"buildCommand": '))291 );292 });293 it('should save entered build command', async () => {294 inquirerPrompt.resolves({295 packageManager: 'npm',296 reporters: [],297 testRunner: 'hyper',298 configType: 'JSON',299 buildCommand: 'npm run build',300 });301 await sut.initialize();302 expect(fs.promises.writeFile).calledWith('stryker.conf.json', sinon.match('"buildCommand": "npm run build"'));303 });304 it('should set "coverageAnalysis" to "off" when the command test runner is chosen', async () => {305 inquirerPrompt.resolves({306 packageManager: 'npm',307 reporters: [],308 testRunner: 'command',309 configType: 'JSON',310 });311 await sut.initialize();312 expect(fs.promises.writeFile).calledWith('stryker.conf.json', sinon.match('"coverageAnalysis": "off"'));313 });314 it('should reject with that error', () => {315 const expectedError = new Error('something');316 fsWriteFile.rejects(expectedError);317 inquirerPrompt.resolves({318 packageManager: 'npm',319 reporters: [],320 testRunner: 'ghost',321 configType: 'JSON',322 });323 return expect(sut.initialize()).to.eventually.be.rejectedWith(expectedError);324 });325 it('should recover when install fails', async () => {326 childExecSync.throws('error');327 inquirerPrompt.resolves({328 packageManager: 'npm',329 reporters: [],330 testRunner: 'ghost',331 configType: 'JSON',332 });333 await sut.initialize();334 expect(out).calledWith('An error occurred during installation, please try it yourself: "npm i --save-dev stryker-ghost-runner"');335 expect(fs.promises.writeFile).called;336 });337 });338 describe('initialize() when no internet', () => {339 it('should log error and continue when fetching test runners', async () => {340 restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/test-runner-plugin').rejects();341 stubMutators('stryker-javascript');342 stubReporters();343 stubPackageClient({ 'stryker-javascript': null, 'stryker-webpack': null });344 inquirerPrompt.resolves({345 packageManager: 'npm',346 reporters: ['clear-text'],347 configType: 'JSON',348 });349 await sut.initialize();350 expect(testInjector.logger.error).calledWith(351 'Unable to reach npms.io (for query /v2/search?q=keywords:@stryker-mutator/test-runner-plugin). Please check your internet connection.'352 );353 expect(fs.promises.writeFile).calledWith('stryker.conf.json', sinon.match('"testRunner": "command"'));354 });355 it('should log error and continue when fetching stryker reporters', async () => {356 stubTestRunners('stryker-awesome-runner');357 stubMutators('stryker-javascript');358 restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/reporter-plugin').rejects();359 inquirerPrompt.resolves({360 packageManager: 'npm',361 reporters: ['clear-text'],362 testRunner: 'awesome',363 configType: 'JSON',364 });365 stubPackageClient({ 'stryker-awesome-runner': null, 'stryker-javascript': null, 'stryker-webpack': null });366 await sut.initialize();367 expect(testInjector.logger.error).calledWith(368 'Unable to reach npms.io (for query /v2/search?q=keywords:@stryker-mutator/reporter-plugin). Please check your internet connection.'369 );370 expect(fs.promises.writeFile).called;371 });372 it('should log warning and continue when fetching custom config', async () => {373 stubTestRunners('stryker-awesome-runner');374 stubMutators();375 stubReporters();376 inquirerPrompt.resolves({377 packageManager: 'npm',378 reporters: ['clear-text'],379 testRunner: 'awesome',380 configType: 'JSON',381 });382 restClientPackage.get.rejects();383 await sut.initialize();384 expect(testInjector.logger.warn).calledWith(385 'Could not fetch additional initialization config for dependency stryker-awesome-runner. You might need to configure it manually'386 );387 expect(fs.promises.writeFile).called;388 });389 });390 SUPPORTED_CONFIG_FILE_EXTENSIONS.forEach((ext) => {391 it(`should log an error and quit when \`stryker.conf${ext}\` file already exists`, async () => {392 existsStub.withArgs(`stryker.conf${ext}`).resolves(true);393 await expect(sut.initialize()).to.be.rejected;394 expect(testInjector.logger.error).calledWith(395 `Stryker config file "stryker.conf${ext}" already exists in the current directory. Please remove it and try again.`396 );397 });398 it(`should log an error and quit when \`.stryker.conf${ext}\` file already exists`, async () => {399 existsStub.withArgs(`.stryker.conf${ext}`).resolves(true);400 await expect(sut.initialize()).to.be.rejected;401 expect(testInjector.logger.error).calledWith(402 `Stryker config file ".stryker.conf${ext}" already exists in the current directory. Please remove it and try again.`403 );404 });405 });406 const stubTestRunners = (...testRunners: string[]) => {407 restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/test-runner-plugin').resolves({408 result: {409 results: testRunners.map((testRunner) => ({ package: { name: testRunner, version: '1.1.1' } })),410 },411 statusCode: 200,412 } as unknown as IRestResponse<PackageInfo[]>);413 };414 const stubMutators = (...mutators: string[]) => {415 restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/mutator-plugin').resolves({416 result: {417 results: mutators.map((mutator) => ({ package: { name: mutator, version: '1.1.1' } })),418 },419 statusCode: 200,420 } as unknown as IRestResponse<PackageInfo[]>);421 };422 const stubReporters = (...reporters: string[]) => {423 restClientSearch.get.withArgs('/v2/search?q=keywords:@stryker-mutator/reporter-plugin').resolves({424 result: {425 results: reporters.map((reporter) => ({ package: { name: reporter, version: '1.1.1' } })),426 },427 statusCode: 200,428 } as unknown as IRestResponse<PackageInfo[]>);429 };430 const stubPackageClient = (packageConfigPerPackage: Record<string, Record<string, unknown> | null>) => {431 Object.keys(packageConfigPerPackage).forEach((packageName) => {432 const pkgConfig: PackageInfo & { initStrykerConfig?: Record<string, unknown> } = {433 keywords: [],434 name: packageName,435 version: '1.1.1',436 };437 const cfg = packageConfigPerPackage[packageName];438 if (cfg) {439 pkgConfig.initStrykerConfig = cfg;440 }441 restClientPackage.get.withArgs(`/${packageName}@1.1.1/package.json`).resolves({442 result: pkgConfig,443 statusCode: 200,444 } as unknown as IRestResponse<PackageInfo[]>);445 });446 };447 interface StrykerInitAnswers {448 preset: string | null;449 testRunner: string;450 reporters: string[];451 packageManager: string;452 }453 function arrangeAnswers(answerOverrides?: Partial<StrykerInitAnswers>) {454 const answers: StrykerInitAnswers = Object.assign(455 {456 packageManager: 'yarn',457 preset: null,458 reporters: ['dimension', 'mars'],459 testRunner: 'awesome',460 },461 answerOverrides462 );463 inquirerPrompt.resolves(answers);464 }465 function resolvePresetConfig(presetConfigOverrides?: Partial<PresetConfiguration>) {466 const presetConfig: PresetConfiguration = {467 config: {},468 dependencies: [],469 guideUrl: '',470 };471 presetMock.createConfig.resolves(Object.assign({}, presetConfig, presetConfigOverrides));472 }473 function expectStrykerConfWritten(expectedRawConfig: string) {474 const [fileName, actualConfig] = fsWriteFile.getCall(0).args;475 expect(fileName).eq('stryker.conf.mjs');476 expect(normalizeWhitespaces(actualConfig as string)).eq(normalizeWhitespaces(expectedRawConfig));477 }...

Full Screen

Full Screen

stryker-initializer.ts

Source:stryker-initializer.ts Github

copy

Full Screen

...113 return this.inquirer.promptTestRunners(testRunnerOptions);114 }115 private async getBuildCommand(selectedTestRunner: PromptOption): Promise<PromptOption> {116 const shouldSkipQuestion = selectedTestRunner.name === 'jest';117 return this.inquirer.promptBuildCommand(shouldSkipQuestion);118 }119 private async selectReporters(): Promise<PromptOption[]> {120 const reporterOptions = await this.client.getTestReporterOptions();121 reporterOptions.push(122 {123 name: 'html',124 pkg: null,125 },126 {127 name: 'clear-text',128 pkg: null,129 },130 {131 name: 'progress',...

Full Screen

Full Screen

stryker-inquirer.ts

Source:stryker-inquirer.ts Github

copy

Full Screen

...36 } else {37 return { name: CommandTestRunner.runnerName, pkg: null };38 }39 }40 public async promptBuildCommand(skip: boolean): Promise<PromptOption> {41 const { buildCommand } = await inquirer.prompt<{ buildCommand: string }>({42 message:43 'What build command should be executed just before running your tests? For example: "npm run build" or "tsc -b" (leave empty when this is not needed).',44 name: 'buildCommand',45 default: 'none',46 when: !skip,47 });48 return { name: buildCommand !== 'none' ? buildCommand : '', pkg: null };49 }50 public async promptReporters(options: PromptOption[]): Promise<PromptOption[]> {51 const answers = await inquirer.prompt<{ reporters: string[] }>({52 choices: options.map((_) => _.name),53 default: ['html', 'clear-text', 'progress'],54 message: 'Which reporter(s) do you want to use?',...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1const { promptBuildCommand } = require('stryker-parent');2promptBuildCommand().then(buildCommand => {3 console.log(buildCommand);4});5{6 "scripts": {7 },8 "dependencies": {9 }10}

Full Screen

Using AI Code Generation

copy

Full Screen

1var promptBuildCommand = require('stryker-parent').promptBuildCommand;2promptBuildCommand('test', function (err, command) {3 if (err) {4 console.error(err);5 process.exit(1);6 }7});8{buildCommand}9{projectRoot}10{configFile}11{strykerVersion}12{nodeModulesPath}13{strykerBin}14{strykerParentPath}15{strykerPath}16{strykerPackagesPath}17{strykerPluginsPath}18{strykerTestRunnerPath}19{strykerTestFrameworkPath}20{strykerMutatorPath}21{strykerReportersPath}22{strykerTranspilersPath}23{strykerTestHooksPath}24{strykerConfigPath}

Full Screen

Using AI Code Generation

copy

Full Screen

1var promptBuildCommand = require('stryker-parent').promptBuildCommand;2promptBuildCommand('npm run build', 'npm run test', function (err) {3 if (err) {4 console.error(err);5 }6});

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run stryker-parent automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful