dao->select('u.*, ug.`group`') ->from(TABLE_USER)->alias('u') ->leftJoin(TABLE_USERGROUP)->alias('ug')->on('u.account = ug.account') ->fetchAll(); $groupPrivs = $uiTester->dao->select('*')->from(TABLE_GROUPPRIV)->fetchAll(); $this->tasks = $uiTester->dao->select('id,name')->from(TABLE_TASK)->orderBy('id')->fetchAll(); $this->prodNames = array_keys($uiTester->dao->select('name')->from(TABLE_PRODUCT)->fetchAll('name')); $this->projNames = array_keys($uiTester->dao->select('name')->from(TABLE_PROJECT)->where('type')->eq('project')->fetchAll('name')); $this->execNames = array_keys($uiTester->dao->select('name')->from(TABLE_PROJECT)->where('type')->eq('sprint')->fetchAll('name')); $this->groupPrivs = []; $this->actions = []; $this->pageTitle = $this->lang->company->dynamic ?? '动态'; $this->userNames = ['all']; //'all'是一个虚拟的用户名,代表所有用户 foreach($users as $u) { $this->users[$u->realname] = $u; $this->userNames[] = $u->realname; } foreach($groupPrivs as $gp) $this->groupPrivs[$gp->group][$gp->module][$gp->method] = $gp; } /** * 用户登录后从数据库中刷新数据(因为增加了登录信息) * Refresh the dynamic data after login * */ private function refreshDataFromDB() { global $uiTester; $this->actions['all'] = []; foreach($this->users as $user) { $action = $uiTester->dao->select('a.*, user.realname as userName, prod.name as prodName, proj.name as projName, exec.name as execName') ->from(TABLE_ACTION)->alias('a') ->leftJoin(TABLE_ACTIONPRODUCT)->alias('ap')->on('a.id = ap.action') ->leftJoin(TABLE_USER)->alias('user')->on('a.actor = user.account') ->leftJoin(TABLE_PRODUCT)->alias('prod')->on('ap.product = prod.id') ->leftJoin(TABLE_PROJECT)->alias('proj')->on('a.project = proj.id') ->leftJoin(TABLE_PROJECT)->alias('exec')->on('a.execution = exec.id') ->where('a.actor')->eq($user->account) ->fetchAll(); $this->actions[$user->realname] = $action; $this->actions['all'] = array_merge($this->actions['all'], $action); } $this->buildActionIndex(); // 预构建所有维度组合的键值映射,使用笛卡尔积生成器替代多层嵌套循环。(效率一样,只是代码更简洁) $this->actionsByKey = []; foreach($this->cartesian([DYNAMIC_MENUS, $this->userNames, $this->prodNames, $this->projNames, $this->execNames]) as $combo) { list($menu, $userName, $prodName, $projName, $execName) = $combo; $key = "{$menu}|{$userName}|{$prodName}|{$projName}|{$execName}"; $this->actionsByKey[$key] = $this->getActionsByDims($menu, $userName, $prodName, $projName, $execName); } } /** * 校验company模块dynamic视图内容 * Verify the content of company dynamic view. * * @return object 成功或失败对象 * @access public */ public function verifyCompanyDynamicContent() { foreach($this->users as $user) { $this->login($user->account); $hasGroupPriv = ($user->account == 'admin') || isset($this->groupPrivs[$user->group]['company']['dynamic']); if($hasGroupPriv) { $this->refreshDataFromDB(); $form = $this->initForm('company', 'dynamic', [], 'appIframe-system'); $form->wait(2); $ret = $this->verifySelectedContent($form, $user); if($ret) return $ret; $ret = $this->verifySearch($form); if($ret) return $ret; } else { $form = null; try { $form = $this->initForm('company', 'dynamic', [], 'appIframe-system'); return $this->failed("用户'{$user->account}'无'{$this->pageTitle}'页面访问权限却能访问页面"); } catch(Exception $e) { // 如果页面对象已初始化,则读取拒绝信息;否则视为无法访问直接跳过 if(is_object($form)) { $form->wait(2); $msg = $form->dom->denied->getText(); // 用户没有team权限应该显示拒绝框 if($msg == $user->account . " " . $this->lang->user->deny) continue; return $this->failed("用户'{$user->account}'无'{$this->pageTitle}'页面访问权限却未显示拒绝框"); } continue; } } } return $this->success('开源版m=company&f=dynamic测试成功'); } /** * 抽查给定索引的任务动态搜索结果 * Verify the search result of company dynamic view. * * @param object $form 页面对象 * @param int $index 任务索引,默认-1表示最后一个任务 * @return object 成功或失败对象 * @access public */ private function verifySearch($form, $index = -1) { // 抽查ID=60的动态是否有数据 $search = [ 'field1' => $this->lang->company->product, 'value1' => $this->lang->product->allProduct, 'field4' => $this->lang->action->objectID]; $form->dom->searchForm->click(); $form->wait(1); foreach($search as $key => $value) { $form->dom->{$key}->picker($value); $form->wait(1); } $index = $index < 0 ? count($this->tasks) - 1 : $index; $form->dom->value4->setValue($this->tasks[$index]->id); $form->wait(1); $form->dom->searchBtn->click(); $form->wait(2); $displayed = $form->dom->searchResult->getText(); $expected = $this->tasks[$index]->name; if($displayed != $expected) return $this->failed("ID={$this->tasks[$index]->id}的任务动态搜索结果为{$displayed},不是预期的任务{$expected}"); return null; } /** * 根据下拉菜单的选择校验内容 * Verify content based on dropdown menu selection * * @param object $form 页面对象 * @param object $user 当前登录用户 * @return object 成功返回null或失败返回对象 */ private function verifySelectedContent($form, $user) { foreach(DYNAMIC_MENUS as $menu) { $header = $this->lang->company->featureBar->dynamic->{$menu} ?? $menu; $form->dom->{$menu}->click(); foreach($this->userNames as $userName) { // 先设置筛选器,再点击周期菜单,确保计数依据最新条件重新渲染。 // 筛选菜单共4个:用户|产品|项目|执行 // 先清除已选用户,再选择当前用户。 if($form->dom->userDeselect) $form->dom->userDeselect->click(); // 如果当前用户是'all‘,则不选择任何用户。 if($userName != 'all' && $form->dom->userSelect->getText() != $userName) $form->dom->userSelect->picker($userName); foreach($this->prodNames as $prodName) { if($form->dom->productSelect->getText() != $prodName) $form->dom->productSelect->picker($prodName); foreach($this->projNames as $projName) { if($form->dom->projectSelect->getText() != $projName) $form->dom->projectSelect->picker($projName); foreach($this->execNames as $execName) { // 具体执行名字前面有'/', 没有选择(即所有执行)不带'/' $prefix = ($execName == $this->lang->execution->common) ? '' : '/'; if($form->dom->execSelect->getText() != $prefix . $execName) $form->dom->execSelect->picker($prefix . $execName); $form->wait(1); // 点击时间周期菜单刷新计数 $form->dom->{$menu}->click(); $form->wait(2); // 读取计数元素前做存在性检查,避免渲染延迟导致空节点。 $countNode = $form->dom->{$menu . 'Count'}; if(!$countNode) { $form->wait(1); $countNode = $form->dom->{$menu . 'Count'}; if(!$countNode) return $this->failed("菜单'{$header}'计数元素未渲染,无法读取数量"); } $displayed = (int) trim($countNode->getText()); $key = "{$menu}|{$userName}|{$prodName}|{$projName}|{$execName}"; $expected = count($this->actionsByKey[$key] ?? []); $userName = $userName == 'all' ? '所有用户' : $userName; if($displayed != $expected) return $this->failed("用户'{$user->realname}'的'|{$header}|{$userName}|{$prodName}|{$projName}|{$execName}|' 动态数量错误,期望{$expected}条,显示{$displayed}条"); } } } } } return null; } /** * 按照用户|产品|项目|执行构建操作索引 * Build action index based on user|product|project|execution. */ private function buildActionIndex() { $this->actionIndex = []; foreach($this->actions as $account => $list) { if($account === 'all') continue; if(empty($list)) continue; foreach($list as $action) { $prod = trim(is_object($action) ? ($action->prodName ?? '') : ($action['prodName'] ?? '')); $proj = trim(is_object($action) ? ($action->projName ?? '') : ($action['projName'] ?? '')); $exec = trim(is_object($action) ? ($action->execName ?? '') : ($action['execName'] ?? '')); $accountKey = trim($account); $base = [$accountKey, $prod, $proj, $exec]; for($mask = 0; $mask < 16; $mask++) { $u = ($mask & 1) ? '__ALL__' : $base[0]; $p = ($mask & 2) ? '__ALL__' : $base[1]; $pr = ($mask & 4) ? '__ALL__' : $base[2]; $e = ($mask & 8) ? '__ALL__' : $base[3]; $k = $u . '|' . $p . '|' . $pr . '|' . $e; if(!isset($this->actionIndex[$k])) $this->actionIndex[$k] = []; $this->actionIndex[$k][] = $action; } } } // 显式添加汇总键,确保'全部'维度必有数据 $aggKey = '__ALL__|__ALL__|__ALL__|__ALL__'; $this->actionIndex[$aggKey] = $this->actions['all'] ?? []; } /** * 根据给定维度组合返回数据 * Get actions based on the combination of given dimensions. * * @param string $menu 时间菜单 * @param string $userName 维度-用户("all"或'realname') * @param string $prodName 维度-产品名 * @param string $projName 维度-项目名 * @param string $execName 维度-执行名 * @return object|null 返回过滤后的列表 */ public function getActionsByDims($menu, $userKey, $prodName, $projName, $execName) { // 将占位标签或“全部”视为 all 维度 $productLabel = $this->lang->company->product ?? '产品'; $projectLabel = $this->lang->company->project ?? '项目'; $execLabel = $this->lang->execution->common ?? '执行'; $allLabel = $this->lang->all ?? '全部'; $prodName = (in_array($prodName, [$productLabel, $allLabel, ''], true)) ? 'all' : trim($prodName); $projName = (in_array($projName, [$projectLabel, $allLabel, ''], true)) ? 'all' : trim($projName); $execName = (in_array($execName, [$execLabel, $allLabel, ''], true)) ? 'all' : trim($execName); $userKey = ($userKey === 'all') ? 'all' : trim($userKey); $u = ($userKey === 'all') ? '__ALL__' : $userKey; $p = ($prodName === 'all') ? '__ALL__' : $prodName; $pr = ($projName === 'all') ? '__ALL__' : $projName; $e = ($execName === 'all') ? '__ALL__' : $execName; $k = $u . '|' . $p . '|' . $pr . '|' . $e; $list = $this->actionIndex[$k] ?? []; return $this->filterItemsByMenu($list, $menu); } /** * 根据时间菜单过滤动态列表。 * Filter dynamic by given period menu. * * @param array $actions 动态列表 * @param string $menu 菜单 * @return array 过滤后的动态列表 */ private function filterItemsByMenu($actions, $menu) { if(empty($actions)) return []; list($beginTS, $endTS) = $this->getBounds($menu); if($beginTS === null || $endTS === null) return $actions; $filtered = []; foreach($actions as $id => $action) { $dateStr = is_object($action) ? ($action->date ?? '') : ($action['date'] ?? ''); if(empty($dateStr)) continue; $ts = strtotime($dateStr); if($ts !== false && $ts >= $beginTS && $ts < $endTS) $filtered[$id] = $action; } return $filtered; } /** * 计算菜单的日期边界,返回 [beginTS, endTS],左闭右开。 * Calculate the date bounds for a given menu period. * * @param string $menu 菜单名称 * @return array 日期边界数组 [beginTS, endTS] */ private function getBounds($menu) { $key = strtolower($menu); switch($key) { case 'today': return [strtotime('today midnight'), strtotime('tomorrow')]; case 'yesterday': return [strtotime('yesterday midnight'), strtotime('today midnight')]; case 'thisweek': return [strtotime('monday this week'), strtotime('monday next week')]; case 'lastweek': return [strtotime('monday last week'), strtotime('monday this week')]; case 'thismonth': $begin = strtotime(date('Y-m-01')); $end = strtotime(date('Y-m-01', strtotime('+1 month', $begin))); return [$begin, $end]; case 'lastmonth': $end = strtotime(date('Y-m-01')); $begin = strtotime(date('Y-m-01', strtotime('-1 month', $end))); return [$begin, $end]; default: return [null, null]; } } /** * 笛卡尔生成器:按顺序产出各集合的组合,避免多层嵌套循环。 * Cartesian generator producing combinations across provided sets. * * @param array $sets [A1, A2, ...],每个元素为一个可遍历集合 * @return Generator 依次yield形如 [a1, a2, ...] 的组合 */ private function cartesian(array $sets) { if(empty($sets)) { yield []; return; } $first = array_shift($sets); foreach($first as $value) { // 递归yield后续集合的组合 foreach($this->cartesian($sets) as $comb) { yield array_merge([$value], $comb); } } } }